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.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/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs b/src/benchmark/Akka.Benchmarks/Remoting/LruBoundedCacheBenchmarks.cs index d01ca7eda69..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); + _pathCache.Cache.GetOrCompute(_cacheHitPath, out var isTemp); } [Benchmark] public void ActorPathCacheMissBenchmark() { - _pathCache.Cache.GetOrCompute(_cacheMissPath); + _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 new file mode 100644 index 00000000000..53da5b90db9 --- /dev/null +++ b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Akka.Cluster.Benchmark.DotTrace.csproj @@ -0,0 +1,20 @@ + + + + Exe + net5.0;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..bd22daee304 --- /dev/null +++ b/src/benchmark/Akka.Cluster.Benchmark.DotTrace/Program.cs @@ -0,0 +1,51 @@ +using System; +using System.Diagnostics; +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(); + var sw = new Stopwatch(); + MeasureProfiler.StartCollectingData(); + for (int i = 0; i < 20; 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(); + 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..baede264b2e 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)); @@ -161,16 +161,31 @@ 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] + //[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..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; @@ -223,6 +224,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 +255,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 @@ -260,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/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/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/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.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index d2b56896a88..c8c3861b358 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -177,18 +177,21 @@ 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) { } 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) { } @@ -199,13 +202,17 @@ 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 abstract Akka.Actor.ActorPath WithUid(long uid); + 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) { } 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; } @@ -408,13 +415,14 @@ 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) { } 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 +505,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 { @@ -1546,15 +1548,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 diff --git a/src/core/Akka.Remote.Tests/RemotingSpec.cs b/src/core/Akka.Remote.Tests/RemotingSpec.cs index 4c3e354222b..5c36790f4fa 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 { @@ -176,7 +177,7 @@ public async Task Remoting_must_support_Ask() Assert.Equal("pong", msg); Assert.IsType>(actorRef); } - + [Fact(Skip = "Racy")] public async Task Ask_does_not_deadlock() { 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/Akka.Remote.csproj b/src/core/Akka.Remote/Akka.Remote.csproj index 5b1f41f9715..9c27515ce9f 100644 --- a/src/core/Akka.Remote/Akka.Remote.csproj +++ b/src/core/Akka.Remote/Akka.Remote.csproj @@ -12,8 +12,10 @@ + + $(DefineConstants);RELEASE 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 9014b35857f..c62fa1ed3b6 100644 --- a/src/core/Akka.Remote/RemoteActorRefProvider.cs +++ b/src/core/Akka.Remote/RemoteActorRefProvider.cs @@ -72,14 +72,14 @@ 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 /// 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); /// @@ -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; } } /// @@ -128,7 +163,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,37 +270,46 @@ public void UnregisterTempActor(ActorPath path) _local.UnregisterTempActor(path); } - private volatile IActorRef _remotingTerminator; - private volatile IActorRef _remoteWatcher; + private IActorRef _remotingTerminator; + private IActorRef _remoteWatcher; - private volatile ActorRefResolveThreadLocalCache _actorRefResolveThreadLocalCache; - private volatile ActorPathThreadLocalCache _actorPathThreadLocalCache; + private ActorRefResolveThreadLocalCache _actorRefResolveThreadLocalCache; + private ActorRefResolveAskCache _actorRefResolveAskCache; + private ActorPathThreadLocalCache _actorPathThreadLocalCache; + //private ActorPathAskResolverCache _actorPathAskResolverCache; /// /// The remote death watcher. /// public IActorRef RemoteWatcher => _remoteWatcher; - private volatile IActorRef _remoteDeploymentWatcher; + private IActorRef _remoteDeploymentWatcher; + private IActorRef _remoteAskHandler; + public IActorRef RefAskCache() => _remoteAskHandler; + public ActorRefAskResolverCache RefAskCacheInst() => _actorRefResolveAskCache.Cache; /// public virtual void Init(ActorSystemImpl system) { _system = system; - _local.Init(system); - _actorRefResolveThreadLocalCache = ActorRefResolveThreadLocalCache.For(system); _actorPathThreadLocalCache = ActorPathThreadLocalCache.For(system); + _actorRefResolveAskCache = ActorRefResolveAskCache.For(system); + //_actorPathAskResolverCache = ActorPathAskResolverCache.For(system); + _local.Init(system); _remotingTerminator = _system.SystemActorOf( RemoteSettings.ConfigureDispatcher(Props.Create(() => new RemotingTerminator(_local.SystemGuardian))), "remoting-terminator"); - _internals = CreateInternals(); + _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); @@ -433,9 +477,10 @@ public Deploy LookUpRemotes(IEnumerable p) return Deploy.None; } + [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); } /// @@ -458,21 +503,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. /// @@ -481,22 +511,67 @@ private bool TryParseCachedPath(string actorPath, out ActorPath path) /// TBD /// TBD /// TBD - public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress) + public IInternalActorRef ResolveActorRefWithLocalAddress(string path, Address localAddress, bool? senderOption = null) + { + if (path is null) + { + _log.Debug("resolve of unknown path [{0}] failed", path); + return InternalDeadLetters; + } + + bool isTempActor = false; + //bool mayBeTempActor = path.Contains("/temp/"); + ActorPath actorPath = null; + if (_actorPathThreadLocalCache != null && _actorRefResolveAskCache != null) + { + if (senderOption.HasValue && senderOption.Value == true) + { + actorPath = _actorPathThreadLocalCache.Cache.GetOrCompute(path, out isTempActor); + } + else + { + actorPath = ExtractRecipientActorPath(path, out isTempActor); + } + } + else // cache not initialized yet + { + ActorPath.TryParse(path, out actorPath); + } + + if (!HasAddress(actorPath.Address)) + return CreateRemoteRef(actorPath, localAddress); + + //the actor's local address was already included in the ActorPath + + if (actorPath is RootActorPath) + return RootGuardian; + + return (IInternalActorRef)ResolveActorRefOpt(path, isTempActor); // so we can use caching + } + + private ActorPath ExtractRecipientActorPath(string path, out bool mayBeTempActor) { - if (TryParseCachedPath(path, out var actorPath)) + ActorPath actorPath = null; + mayBeTempActor = false; + //if (mayBeTempActor) { - //the actor's local address was already included in the ActorPath - if (HasAddress(actorPath.Address)) + var maybeRef = _actorRefResolveAskCache.Cache.GetOrNull(path); + if (maybeRef != null) { - if (actorPath is RootActorPath) - return RootGuardian; - return (IInternalActorRef)ResolveActorRef(path); // so we can use caching + actorPath = maybeRef.Path; + mayBeTempActor = true; } + //actorPath = + // _actorPathAskResolverCache.Cache.GetOrNull(path); + } - return CreateRemoteRef(new RootActorPath(actorPath.Address) / actorPath.ElementsWithUid, localAddress); + if (actorPath == null) + { + actorPath = + _actorPathThreadLocalCache.Cache.GetOrCompute(path, out mayBeTempActor); } - _log.Debug("resolve of unknown path [{0}] failed", path); - return InternalDeadLetters; + + return actorPath; } @@ -531,17 +606,29 @@ 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; // using thread local LRU cache, which will call InternalResolveActorRef // if the value is not cached - if (_actorRefResolveThreadLocalCache == null) + if (_actorRefResolveThreadLocalCache == null || _actorRefResolveAskCache == null) { - return InternalResolveActorRef(path); // cache not initialized yet + // cache not initialized yet, should never happen + return InternalResolveActorRef(path); } - return _actorRefResolveThreadLocalCache.Cache.GetOrCompute(path); + + IActorRef actorRef = null; + if (checkAsk) + { + actorRef = _actorRefResolveAskCache.Cache.GetOrNull(path); + } + return actorRef??_actorRefResolveThreadLocalCache.Cache.GetOrCompute(path); } /// @@ -592,19 +679,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/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/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 25e4eb6cdad..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,18 +110,49 @@ 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 = + 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 /// - internal class Remoting : RemoteTransport + 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 @@ -146,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; } } @@ -196,19 +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)); + + _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)); + _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()))); @@ -375,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 f837bb0148d..2d2a2b30e6a 100644 --- a/src/core/Akka.Remote/Serialization/ActorPathCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorPathCache.cs @@ -8,6 +8,8 @@ using System; using Akka.Actor; using System.Threading; +using System.Collections.Generic; +using Akka.Remote.Serialization.BitFasterBased; namespace Akka.Remote.Serialization { @@ -16,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(); @@ -30,26 +35,173 @@ 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.TryAdd(actorPath,actorPathObj); + } + + } + + internal sealed class ActorPathBitfasterCache + { + public readonly FastConcurrentLru _cache = + new FastConcurrentLru(Environment.ProcessorCount, + 1030, FastHashComparer.Default); + public readonly FastConcurrentLru _rootCache = + new FastConcurrentLru(Environment.ProcessorCount, + 540, FastHashComparer.Default); + public ActorPath GetOrCompute(string k, out bool isTempActor) + { + //if (mayBeTempActor) + //{ + // return ParsePath(k); + //} + if (_cache.TryGet(k, out ActorPath outPath)) + { + isTempActor = true; + return outPath; + } + + outPath = ParsePath(k); + + isTempActor = k.Contains("/temp/"); + if (outPath != null && !isTempActor) + { + + _cache.TryAdd(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.TryAdd(rootPath, actorPath); + } + + if (!ActorPath.TryParse(actorPath, absoluteUri, out actorPath))//, out bool mayBeTemp)) + return null; + + return actorPath; + } + } /// /// INTERNAL API /// 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) { - if (ActorPath.TryParse(k, out var actorPath)) - return actorPath; - return null; + ActorPath actorPath; + + 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 (!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))//, out bool isTemp)) + return null; + + return actorPath; } 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 2bd7de5b453..28116df2894 100644 --- a/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs +++ b/src/core/Akka.Remote/Serialization/ActorRefResolveCache.cs @@ -5,8 +5,11 @@ // //----------------------------------------------------------------------- +using System; +using System.Collections.Generic; using System.Threading; using Akka.Actor; +using Akka.Remote.Serialization.BitFasterBased; using Akka.Util.Internal; namespace Akka.Remote.Serialization @@ -23,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) @@ -31,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.TryAdd(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.TryAdd(k, outRef); + } + + return outRef; + } + } /// /// INTERNAL API @@ -48,7 +135,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; } @@ -58,11 +146,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/BitFasterBased/LruWithOptimizedRemove.cs b/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs new file mode 100644 index 00000000000..b0909c08de1 --- /dev/null +++ b/src/core/Akka.Remote/Serialization/BitFasterBased/LruWithOptimizedRemove.cs @@ -0,0 +1,679 @@ +// //----------------------------------------------------------------------- +// // +// // 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 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 + { + /// + /// 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 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 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. + protected H hitCounter; + + public TemplateConcurrentLru( + int concurrencyLevel, + int capacity, + IEqualityComparer comparer, + LruPolicy 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) + { + LruItem 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.TryGetValue(key, out var existing)) + { + bool retVal = GetOrDiscard(existing, out value); + //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(); + // } + // } + //} + + 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(LruItem 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.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(); + // } + // } + //} + + + 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(LruItem item, ItemDestination where) + { + item.SetUnaccessed(); + 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: + if (item.ShouldFastDiscard == false) + { + + 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.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; + } + } + + 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.Remote/Serialization/LruBoundedCache.cs b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs index e2a99fde2f3..62b0284e616 100644 --- a/src/core/Akka.Remote/Serialization/LruBoundedCache.cs +++ b/src/core/Akka.Remote/Serialization/LruBoundedCache.cs @@ -6,7 +6,11 @@ //----------------------------------------------------------------------- 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 { @@ -25,14 +29,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 @@ -53,6 +67,10 @@ public static int OfString(string 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; @@ -82,6 +100,82 @@ 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); + } + + + } + + /// + /// 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 int GetHashCode(string s) + { + return FastHash.OfStringFast(s); + } } /// @@ -103,6 +197,8 @@ public CacheStatistics(int entries, int maxProbeDistance, double averageProbeDis public double AverageProbeDistance { get; } } + + /// /// INTERNAL API /// @@ -117,7 +213,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,11 +224,13 @@ protected LruBoundedCache(int capacity, int evictAgeThreshold) Capacity = capacity; EvictAgeThreshold = evictAgeThreshold; + _keyComparer = keyComparer; _mask = Capacity - 1; _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; } @@ -144,6 +242,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; @@ -174,7 +273,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; @@ -186,18 +285,49 @@ 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++; } } public TValue GetOrCompute(TKey k) { - var h = Hash(k); + TryGetOrCompute(k, out var value); + return value; + } + + public bool TryGetOrCompute(TKey k, out TValue value) + { + var h = _keyComparer.GetHashCode(k); unchecked { _epoch += 1; } var position = h & _mask; @@ -207,7 +337,7 @@ public TValue GetOrCompute(TKey k) { if (_values[position] == null) { - var value = Compute(k); + value = Compute(k); if (IsCacheable(value)) { _keys[position] = k; @@ -215,7 +345,7 @@ public TValue GetOrCompute(TKey k) _hashes[position] = h; _epochs[position] = _epoch; } - return value; + return false; } else { @@ -224,21 +354,70 @@ 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])) + else if (_hashes[position] == h && _keyComparer.Equals(k, _keys[position])) { // Update usage _epochs[position] = _epoch; - return _values[position]; + value = _values[position]; + return true; } else { // This is not our slot yet position = (position + 1) & _mask; - probeDistance = probeDistance + 1; + 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++; } } } @@ -333,9 +512,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.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 bde6e0baf27..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 @@ -202,12 +203,15 @@ public AckAndMessage(Ack ackOption, Message messageOption) internal abstract class AkkaPduCodec { protected readonly ActorSystem System; - protected readonly AddressThreadLocalCache AddressCache; - + protected readonly ActorPathThreadLocalCache ActorPathCache; + protected readonly ActorRefResolveAskCache AskRefCache; + //protected readonly ActorPathAskResolverCache AskPathCache; protected AkkaPduCodec(ActorSystem system) { System = system; - AddressCache = AddressThreadLocalCache.For(system); + ActorPathCache = ActorPathThreadLocalCache.For(system); + AskRefCache = ActorRefResolveAskCache.For(system); + //AskPathCache = ActorPathAskResolverCache.For(system); } /// @@ -286,9 +290,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 @@ -301,7 +311,7 @@ public abstract ByteString ConstructMessage(Address localAddress, IActorRef reci /// /// TBD /// - internal class AkkaPduProtobuffCodec : AkkaPduCodec + internal class AkkaPduMessagePackCodec : AkkaPduCodec { /// /// TBD @@ -400,6 +410,33 @@ public override ByteString ConstructHeartbeat() /// 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 /// @@ -409,7 +446,9 @@ public override ByteString ConstructHeartbeat() /// TBD public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvider provider, Address localAddress) { - var ackAndEnvelope = AckAndEnvelopeContainer.Parser.ParseFrom(raw); + var ackAndEnvelope = MessagePackSerializer.Deserialize(raw.Memory, + MessagePackSerializerOptions.Standard); + //var ackAndEnvelope = AckAndEnvelopeContainer.Parser.ParseFrom(raw); Ack ackOption = null; @@ -425,23 +464,337 @@ public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvi var envelopeContainer = ackAndEnvelope.Envelope; if (envelopeContainer != null) { - var recipient = provider.ResolveActorRefWithLocalAddress(envelopeContainer.Recipient.Path, localAddress); - Address recipientAddress; - if (AddressCache != 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) { - recipientAddress = AddressCache.Cache.GetOrCompute(envelopeContainer.Recipient.Path); + unchecked + { + seqOption = new SeqNo((long)envelopeContainer.Seq); //proto takes a ulong + } } - else + + 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 { - ActorPath.TryParseAddress(envelopeContainer.Recipient.Path, out recipientAddress); + 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 + /// + internal class AkkaPduProtobuffCodec : 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; + + /// + /// TBD + /// + /// TBD + /// TBD + /// TBD + /// TBD + public override AckAndMessage DecodeMessage(ByteString raw, IRemoteActorRefProvider provider, Address localAddress) + { + 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.Path, localAddress, false); + + //todo get parsed address from provider + 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) { @@ -450,6 +803,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); } } @@ -476,13 +830,29 @@ 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) }; - 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) + { + //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); + } + } + if (seqOption != null) { envelope.Seq = (ulong)seqOption.RawValue; } else envelope.Seq = SeqUndefined; if (ackOption != null) { ackAndEnvelope.Ack = AckBuilder(ackOption); } envelope.Message = serializedMessage; @@ -565,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.Tests/Actor/ActorPathSpec.cs b/src/core/Akka.Tests/Actor/ActorPathSpec.cs index acd081c6494..b22f2eedc12 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()); @@ -100,15 +99,55 @@ 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() { - 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] + public void Supports_jumbo_actor_name_length() + { + 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(nameSize); + actorPath.Name.All(n => n == 'a').ShouldBe(true); + + var result = actorPath.ToStringWithAddress(); + result.ShouldBe(path); } [Fact] @@ -196,7 +235,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); @@ -238,7 +277,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(); } } 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 8491dd1e886..0ad5b0327c7 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,8 @@ public Surrogate(string path) /// The encapsulated by this surrogate. public ISurrogated FromSurrogate(ActorSystem system) { - if (TryParse(Path, out var path)) - { - return path; - } - - return null; + TryParse(Path, out var path); + return path; } #region Equality @@ -72,25 +67,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; - return Equals(other.ToSurrogate(null)); //TODO: not so sure if this is OK + if (other is null) return false; + return StringComparer.Ordinal.Equals(Path, other.ToSerializationFormat()); } /// 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 +110,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') || @@ -132,17 +121,17 @@ 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) { 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 { @@ -152,43 +141,92 @@ private static bool Validate(string chars) return true; } + 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) { - Name = name; - Address = address; + _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 parent path. + /// The parentPath. /// The name. /// The uid. protected ActorPath(ActorPath parentPath, string name, long uid) { - Address = parentPath.Address; - Uid = uid; - Name = name; + _parent = parentPath; + _address = parentPath._address; + _depth = parentPath._depth + 1; + _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; - internal static readonly string[] EmptyElements = { }; + /// + /// The path of the parent to this actor. + /// + public ActorPath Parent => _parent; + + /// + /// The the depth of the actor. + /// + public int Depth => _depth; /// /// Gets the elements. /// /// The elements. - public abstract IReadOnlyList Elements { get; } + public IReadOnlyList Elements + { + get + { + 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] = p._name; + p = p._parent; + } + return b.MoveToImmutable(); + } + } /// /// INTERNAL API. @@ -202,70 +240,106 @@ internal IReadOnlyList ElementsWithUid { get { - if (this is RootActorPath) return EmptyElements; - 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.MoveToImmutable(); } } - /// - /// 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; } - - /// - /// The path of the parent to this actor. - /// - public abstract ActorPath Parent { get; } + [JsonIgnore] + public ActorPath Root => ParentOf(0); /// public bool Equals(ActorPath other) { - if (other == null) + 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; - ActorPath a = this; - ActorPath b = other; - for (; ; ) + 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; } } /// - public abstract int CompareTo(ActorPath other); + public int CompareTo(ActorPath other) + { + if (_depth == 0) + { + if (other is null || other._depth > 0) 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 (right is null) + return 1; + if (left is null) + return -1; + + if (left._depth == 0) + return left.CompareTo(right); + + if (right._depth == 0) + 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 (_depth == 0) + { + 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 @@ -290,14 +364,35 @@ public bool Equals(ActorPath other) 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; } + /// + /// 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 . /// @@ -308,12 +403,9 @@ 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}"); + return TryParse(path, out var actorPath) + ? actorPath + : throw new UriFormatException($"Can not parse an ActorPath: {path}"); } /// @@ -325,42 +417,78 @@ 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)) + { + actorPath = null; + return false; + } - if (!TryParseAddress(path, out var address, out var absoluteUri)) return false; - var spanified = absoluteUri; + return TryParse(new RootActorPath(address), absoluteUri, out actorPath);//, out bool isTemp); + } - // check for Uri fragment here - var nextSlash = 0; + /// + /// 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); //, out bool isTemp); + } - actorPath = new RootActorPath(address); + /// + /// 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 + ) + //, out bool isTemp) + { + actorPath = basePath; + //isTemp = false; + // check for Uri fragment here + int nextSlash; do { - nextSlash = spanified.IndexOf('/'); + nextSlash = absoluteUri.IndexOf('/'); if (nextSlash > 0) { - actorPath /= spanified.Slice(0, nextSlash).ToString(); + //var name = absoluteUri.Slice(0, nextSlash).ToString(); + actorPath = new ChildActorPath(actorPath, absoluteUri.Slice(0, nextSlash).ToString(), ActorCell.UndefinedUid); } - else if (nextSlash < 0 && spanified.Length > 0) // final segment + else if (nextSlash < 0 && absoluteUri.Length > 0) // final segment { - var fragLoc = spanified.IndexOf('#'); + //if (string.Equals(actorPath._name, "temp", + // StringComparison.OrdinalIgnoreCase)) + //{ + // isTemp = true; + //} + var fragLoc = absoluteUri.IndexOf('#'); if (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); + //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); } else { - actorPath /= spanified.ToString(); + actorPath = new ChildActorPath(actorPath, absoluteUri.ToString(), ActorCell.UndefinedUid); } - + } - spanified = spanified.Slice(nextSlash + 1); - } while (nextSlash >= 0); + absoluteUri = absoluteUri.Slice(nextSlash + 1); + } + while (nextSlash >= 0); return true; } @@ -383,153 +511,103 @@ 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; - - var spanified = path.AsSpan(); - absoluteUri = spanified; - - var firstColonPos = spanified.IndexOf(':'); + address = default; - if (firstColonPos == -1) // not an absolute Uri + if (!TryParseParts(path.AsSpan(), out var addressSpan, out absoluteUri)) return false; - var fullScheme = SpanHacks.ToLowerInvariant(spanified.Slice(0, firstColonPos)); - if (!fullScheme.StartsWith("akka")) + if (!Address.TryParse(addressSpan, out address)) return false; - spanified = spanified.Slice(firstColonPos + 1); - if (spanified.Length < 2 || !(spanified[0] == '/' && spanified[1] == '/')) - return false; - - spanified = spanified.Slice(2); // move past the double // - var firstAtPos = spanified.IndexOf('@'); - var sysName = string.Empty; - - 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; - } - - // 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. - */ - var host = string.Empty; - - // check for IPV6 first - var openBracket = spanified.IndexOf('['); - var closeBracket = spanified.IndexOf(']'); - if (openBracket > -1 && closeBracket > openBracket) - { - // 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; + return true; + } - spanified = spanified.Slice(secondColonPos + 1); - } - else + /// + /// 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 + || path.Length strPort; - if (actorPathSlash == -1) + + //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) { - strPort = spanified; + address = path; + absoluteUri = "/".AsSpan(); // RELY ON THE JIT } else { - strPort = spanified.Slice(0, actorPathSlash); - } - - 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; + address = path.Slice(0, firstAtPos + 3 + nextSlash); + absoluteUri = path.Slice(address.Length); } - return false; + return true; } /// /// 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 = prefix.Length < 1024 ? stackalloc char[prefix.Length + 1] : new char[prefix.Length + 1]; + prefix.CopyTo(buffer); + buffer[buffer.Length - 1] = '/'; + return buffer.ToString(); //todo use string.Create() when available } - - // 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 = totalLength < 1024 ? stackalloc char[totalLength] : new char[totalLength]; + prefix.CopyTo(buffer); - p = p.Parent; + var offset = buffer.Length; + p = this; + while (p._depth > 0) + { + var name = p._name.AsSpan(); + offset -= name.Length + 1; + buffer[offset] = '/'; + name.CopyTo(buffer.Slice(offset + 1, name.Length)); + p = p._parent; + } + return buffer.ToString(); //todo use string.Create() when available } - - return new string(buffer); } /// @@ -540,13 +618,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()); } /// @@ -555,10 +633,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(); } /// @@ -571,15 +646,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; } } @@ -587,8 +661,7 @@ public override int GetHashCode() /// public override bool Equals(object obj) { - var other = obj as ActorPath; - return Equals(other); + return Equals(obj as ActorPath); } /// @@ -599,7 +672,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; } /// @@ -610,7 +683,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); } /// @@ -619,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 /// @@ -648,12 +729,24 @@ public string ToSerializationFormatWithAddress(Address address) return result; } - private string AppendUidFragment(string withAddress) + private string lazyDefaultAddress; + public string ToSerializationFormatWithDefaultAddress(Address address) { - if (Uid == ActorCell.UndefinedUid) - return withAddress; + if (lazyDefaultAddress == null) + { + lazyDefaultAddress = ToSerializationFormatWithAddress(address); + } + return lazyDefaultAddress; + } - return String.Concat(withAddress, "#", Uid.ToString()); + private string AppendUidFragment(string withAddress) + { + return _uid != ActorCell.UndefinedUid + ? + //string.Concat(withAddress,"#",Uid.ToString()) + string.Concat(withAddress, "#", Uid.ToString()) + //$"{withAddress}#{_uid}" + : withAddress; } /// @@ -670,10 +763,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()); } /// @@ -683,7 +776,7 @@ public string ToStringWithAddress(Address address) /// TBD public static string FormatPathElements(IEnumerable pathElements) { - return String.Join("/", pathElements); + return string.Join("/", pathElements); } /// @@ -700,7 +793,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. @@ -712,39 +805,13 @@ public RootActorPath(Address address, string name = "") { } - /// - public override ActorPath Parent => null; - - public override IReadOnlyList Elements => EmptyElements; - - /// - [JsonIgnore] - public override ActorPath Root => this; - - /// - public override ActorPath WithUid(long uid) - { - if (uid == 0) - return this; - throw new NotSupportedException("RootActorPath must have undefined Uid"); - } - - /// - public override int CompareTo(ActorPath other) - { - if (other is ChildActorPath) return 1; - return Compare(ToString(), other.ToString(), StringComparison.Ordinal); - } } /// /// 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; - /// /// Initializes a new instance of the class. /// @@ -754,87 +821,6 @@ public class ChildActorPath : ActorPath public ChildActorPath(ActorPath parentPath, string name, long uid) : base(parentPath, name, uid) { - _name = name; - _parent = parentPath; - } - - /// - public override ActorPath Parent => _parent; - - public override IReadOnlyList Elements - { - 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) - { - if (uid == Uid) - return this; - return new ChildActorPath(_parent, _name, uid); - } - - /// - 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; - var leftRoot = left as RootActorPath; - if (leftRoot != null) - return leftRoot.CompareTo(right); - var rightRoot = right as RootActorPath; - if (rightRoot != null) - return -rightRoot.CompareTo(left); - var nameCompareResult = Compare(left.Name, right.Name, StringComparison.Ordinal); - if (nameCompareResult != 0) - return nameCompareResult; - return InternalCompareTo(left.Parent, right.Parent); } } } diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 7208047ff44..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; @@ -509,9 +514,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/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/Address.cs b/src/core/Akka/Actor/Address.cs index 81398855cc6..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) @@ -246,7 +252,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,11 +305,102 @@ 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.StartsWith("//".AsSpan()) == false) + return false; + + span = span.Slice(2); // move past the double // + var firstAtPos = span.IndexOf('@'); + + + if (firstAtPos == -1) + { + // dealing with an absolute local Uri + address = new Address(fullScheme.ToString(), span.ToString()); + return true; + } + // dealing with a remote Uri + string 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. /// - public class AddressSurrogate : ISurrogate + public sealed class AddressSurrogate : ISurrogate { /// /// TBD 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; } diff --git a/src/core/Akka/Akka.csproj b/src/core/Akka/Akka.csproj index 2f0d47d7804..4fcb3478bb7 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 @@ -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..b35d4ef3e76 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 { @@ -96,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] @@ -227,37 +230,70 @@ private void ReleaseWorker() private void RequestWorker() { - _pool.QueueUserWorkItem(() => + _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; } - }); + } + // We're done processing items on the current thread + finally { _currentThreadIsRunningTasks = false; } } } @@ -299,12 +335,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,state); + } - return _workQueue.TryAdd(work); + private static void ThrowNullWorkHelper(Action work) + { + throw new ArgumentNullException(nameof(work), "Work item cannot be null."); } /// @@ -365,11 +406,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 +434,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 +447,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 +456,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 +783,366 @@ 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; + + //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; + } + } #endregion } 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 => {})); 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/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; } 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.