From 7f9aea4fa77a1655ce3a18542a0d7a1878dfecd3 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 12 Jun 2018 18:11:37 -0500 Subject: [PATCH 01/14] upgraded to XUnit 2.3.1 and VsTest SDK 15.7.2 --- src/common.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common.props b/src/common.props index faceef1c8ae..c35943e16dc 100644 --- a/src/common.props +++ b/src/common.props @@ -9,8 +9,8 @@ $(NoWarn);CS1591 - 2.3.0 - 15.3.0 + 2.3.1 + 15.7.2 akka;actors;actor model;Akka;concurrency From 5cf4ea61dfb786d1c3ca9e37c9bafa585192df9c Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 13 Jun 2018 21:50:47 -0500 Subject: [PATCH 02/14] forced API tests to copy their output --- src/core/Akka.API.Tests/Akka.API.Tests.csproj | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/core/Akka.API.Tests/Akka.API.Tests.csproj b/src/core/Akka.API.Tests/Akka.API.Tests.csproj index 6bb230ee621..cf2293f76f0 100644 --- a/src/core/Akka.API.Tests/Akka.API.Tests.csproj +++ b/src/core/Akka.API.Tests/Akka.API.Tests.csproj @@ -6,6 +6,40 @@ net452 + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + From fff8ae4d58fa234bb30c045ac0443dbcd70b669f Mon Sep 17 00:00:00 2001 From: zbynek001 Date: Thu, 14 Jun 2018 22:18:59 +0200 Subject: [PATCH 03/14] RestartShard handled on Shard (#3509) --- src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs index 6ce0bc9eb20..489cc8eed58 100644 --- a/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs +++ b/src/contrib/cluster/Akka.Cluster.Sharding/Shard.cs @@ -450,6 +450,8 @@ public static bool HandleCommand(this TShard shard, object message) wher case Shard.IShardQuery sq: shard.HandleShardRegionQuery(sq); return true; + case ShardRegion.RestartShard _: + return true; case var _ when shard.ExtractEntityId(message) != null: shard.DeliverMessage(message, shard.Context.Sender); return true; From 810f8fc5d6601cd65d409ad82977f7c19265b6be Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 14 Jun 2018 15:32:28 -0500 Subject: [PATCH 04/14] bumped v1.4 branch to v1.4 versions (#3511) --- RELEASE_NOTES.md | 2 +- src/common.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 49092bd036f..cddd0b495eb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,4 @@ -#### 1.3.9 June 04 2018 #### +#### 1.4.0 June 14 2018 #### Placeholder for nightlies. #### 1.3.8 June 04 2018 #### diff --git a/src/common.props b/src/common.props index c35943e16dc..4daed09cfbd 100644 --- a/src/common.props +++ b/src/common.props @@ -2,7 +2,7 @@ Copyright © 2013-2018 Akka.NET Team Akka.NET Team - 1.3.9 + 1.4.0 http://getakka.net/images/akkalogo.png https://github.com/akkadotnet/akka.net https://github.com/akkadotnet/akka.net/blob/master/LICENSE From 913fc75b9e2d7c93e34d340c0f503dd50ae7024a Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 21 Jun 2018 18:42:50 -0700 Subject: [PATCH 05/14] Add Exception overloads to all logging methods; close #3424 --- src/core/Akka.Tests/Event/LoggerSpec.cs | 65 +++++++++++++++++++++++ src/core/Akka/Event/Debug.cs | 15 +++++- src/core/Akka/Event/Error.cs | 19 ------- src/core/Akka/Event/ILoggingAdapter.cs | 48 +++++++++++++++++ src/core/Akka/Event/Info.cs | 15 +++++- src/core/Akka/Event/LogEvent.cs | 11 +++- src/core/Akka/Event/LoggingAdapterBase.cs | 29 +++++++--- src/core/Akka/Event/Warning.cs | 13 +++++ 8 files changed, 186 insertions(+), 29 deletions(-) diff --git a/src/core/Akka.Tests/Event/LoggerSpec.cs b/src/core/Akka.Tests/Event/LoggerSpec.cs index 17416818934..eb8ab21be74 100644 --- a/src/core/Akka.Tests/Event/LoggerSpec.cs +++ b/src/core/Akka.Tests/Event/LoggerSpec.cs @@ -16,11 +16,75 @@ using Akka.TestKit; using Xunit; using Xunit.Abstractions; +using FluentAssertions; namespace Akka.Tests.Event { public class LoggerSpec : AkkaSpec { + public static readonly Config Config = @"akka.loglevel = DEBUG"; + + public LoggerSpec(ITestOutputHelper helper) : base(Config, helper) { } + + [Theory] + [InlineData(LogLevel.ErrorLevel, false, "foo", new object[] { })] + [InlineData(LogLevel.ErrorLevel, true, "foo", new object[] { })] + [InlineData(LogLevel.ErrorLevel, false, "foo {0}", new object[] { 1 })] + [InlineData(LogLevel.ErrorLevel, true, "foo {0}", new object[] { 1 })] + [InlineData(LogLevel.WarningLevel, false, "foo", new object[] { })] + [InlineData(LogLevel.WarningLevel, true, "foo", new object[] { })] + [InlineData(LogLevel.WarningLevel, false, "foo {0}", new object[] { 1 })] + [InlineData(LogLevel.WarningLevel, true, "foo {0}", new object[] { 1 })] + [InlineData(LogLevel.InfoLevel, false, "foo", new object[] { })] + [InlineData(LogLevel.InfoLevel, true, "foo", new object[] { })] + [InlineData(LogLevel.InfoLevel, false, "foo {0}", new object[] { 1 })] + [InlineData(LogLevel.InfoLevel, true, "foo {0}", new object[] { 1 })] + [InlineData(LogLevel.DebugLevel, false, "foo", new object[]{})] + [InlineData(LogLevel.DebugLevel, true, "foo", new object[] { })] + [InlineData(LogLevel.DebugLevel, false, "foo {0}", new object[] { 1 })] + [InlineData(LogLevel.DebugLevel, true, "foo {0}", new object[] { 1 })] + public void LoggingAdapter_should_log_all_information(LogLevel logLevel, bool includeException, string formatStr, object [] args) + { + Sys.EventStream.Subscribe(TestActor, typeof(LogEvent)); + var msg = args != null ? string.Format(formatStr, args) : formatStr; + var ex = new Exception(); + switch (logLevel) + { + case LogLevel.DebugLevel when includeException: + Log.Debug(ex, formatStr, args); + break; + case LogLevel.DebugLevel: + Log.Debug(formatStr, args); + break; + case LogLevel.InfoLevel when includeException: + Log.Info(ex, formatStr, args); + break; + case LogLevel.InfoLevel: + Log.Info(formatStr, args); + break; + case LogLevel.WarningLevel when includeException: + Log.Warning(ex, formatStr, args); + break; + case LogLevel.WarningLevel: + Log.Warning(formatStr, args); + break; + case LogLevel.ErrorLevel when includeException: + Log.Error(ex, formatStr, args); + break; + case LogLevel.ErrorLevel: + Log.Error(formatStr, args); + break; + } + + var log = ExpectMsg(); + log.Message.ToString().Should().Be(msg); + log.LogLevel().Should().Be(logLevel); + if (includeException) + log.Cause.Should().Be(ex); + else + log.Cause.Should().BeNull(); + } + [Fact] public async Task LoggingBus_should_stop_all_loggers_on_termination() { @@ -46,3 +110,4 @@ public async Task LoggingBus_should_stop_all_loggers_on_termination() } } } + diff --git a/src/core/Akka/Event/Debug.cs b/src/core/Akka/Event/Debug.cs index 2df6a54bf3c..ded596c6d6e 100644 --- a/src/core/Akka/Event/Debug.cs +++ b/src/core/Akka/Event/Debug.cs @@ -20,11 +20,24 @@ public class Debug : LogEvent /// The source that generated the log event. /// The type of logger used to log the event. /// The message that is being logged. - public Debug(string logSource, Type logClass, object message) + public Debug(string logSource, Type logClass, object message) + : this(null, logSource, logClass, message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception that generated the log event. + /// The source that generated the log event. + /// The type of logger used to log the event. + /// The message that is being logged. + public Debug(Exception cause, string logSource, Type logClass, object message) { LogSource = logSource; LogClass = logClass; Message = message; + Cause = cause; } /// diff --git a/src/core/Akka/Event/Error.cs b/src/core/Akka/Event/Error.cs index bab443ede84..b6b0a7fd5d3 100644 --- a/src/core/Akka/Event/Error.cs +++ b/src/core/Akka/Event/Error.cs @@ -29,11 +29,6 @@ public Error(Exception cause, string logSource, Type logClass, object message) Message = message; } - /// - /// The exception that caused the log event. - /// - public Exception Cause { get; private set; } - /// /// Retrieves the used to classify this event. /// @@ -42,19 +37,5 @@ public override LogLevel LogLevel() { return Event.LogLevel.ErrorLevel; } - - /// - /// Returns a that represents this instance. - /// - /// A that represents this instance. - public override string ToString() - { - var cause = Cause; - var causeStr = cause == null ? "Unknown" : cause.ToString(); - var errorStr = string.Format("[{0}][{1}][Thread {2}][{3}] {4}{5}Cause: {6}", - LogLevel().ToString().Replace("Level", "").ToUpperInvariant(), Timestamp, - Thread.ManagedThreadId.ToString().PadLeft(4, '0'), LogSource, Message, Environment.NewLine, causeStr); - return errorStr; - } } } diff --git a/src/core/Akka/Event/ILoggingAdapter.cs b/src/core/Akka/Event/ILoggingAdapter.cs index 6793ea783dc..d4fb8635709 100644 --- a/src/core/Akka/Event/ILoggingAdapter.cs +++ b/src/core/Akka/Event/ILoggingAdapter.cs @@ -48,6 +48,14 @@ public interface ILoggingAdapter /// An optional list of items used to format the message. void Debug(string format, params object[] args); + /// + /// Logs a message and associated exception. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. + void Debug(Exception cause, string format, params object[] args); + /// /// Logs a message. /// @@ -55,6 +63,14 @@ public interface ILoggingAdapter /// An optional list of items used to format the message. void Info(string format, params object[] args); + /// + /// Logs a message and associated exception. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. + void Info(Exception cause, string format, params object[] args); + /// /// Logs a message. /// @@ -62,6 +78,14 @@ public interface ILoggingAdapter /// An optional list of items used to format the message. void Warning(string format, params object[] args); + /// + /// Logs a message and associated exception. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. + void Warning(Exception cause, string format, params object[] args); + /// /// Logs a message. /// @@ -136,6 +160,14 @@ public bool IsEnabled(LogLevel logLevel) /// An optional list of items used to format the message. public void Debug(string format, params object[] args) { } + /// + /// Logs a message and associated exception. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. + public void Debug(Exception cause, string format, params object[] args){ } + /// /// Logs a message. /// @@ -143,6 +175,14 @@ public void Debug(string format, params object[] args) { } /// An optional list of items used to format the message. public void Info(string format, params object[] args) { } + /// + /// Logs a message and associated exception. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. + public void Info(Exception cause, string format, params object[] args){ } + /// /// Obsolete. Use instead! /// @@ -157,6 +197,14 @@ public void Warn(string format, params object[] args) { } /// An optional list of items used to format the message. public void Warning(string format, params object[] args) { } + /// + /// Logs a message and associated exception. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. + public void Warning(Exception cause, string format, params object[] args){ } + /// /// Logs a message. /// diff --git a/src/core/Akka/Event/Info.cs b/src/core/Akka/Event/Info.cs index adbca965c50..357d7110a46 100644 --- a/src/core/Akka/Event/Info.cs +++ b/src/core/Akka/Event/Info.cs @@ -20,8 +20,21 @@ public class Info : LogEvent /// The source that generated the log event. /// The type of logger used to log the event. /// The message that is being logged. - public Info(string logSource, Type logClass, object message) + public Info(string logSource, Type logClass, object message) + : this(null, logSource, logClass, message) { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception that generated the log event. + /// The source that generated the log event. + /// The type of logger used to log the event. + /// The message that is being logged. + public Info(Exception cause, string logSource, Type logClass, object message) + { + Cause = cause; LogSource = logSource; LogClass = logClass; Message = message; diff --git a/src/core/Akka/Event/LogEvent.cs b/src/core/Akka/Event/LogEvent.cs index a6d2fa51012..2fad64b39b4 100644 --- a/src/core/Akka/Event/LogEvent.cs +++ b/src/core/Akka/Event/LogEvent.cs @@ -52,6 +52,11 @@ protected LogEvent() Thread = Thread.CurrentThread; } + /// + /// The exception that caused the log event. Can be null + /// + public Exception Cause { get; protected set; } + /// /// The timestamp that this event occurred. /// @@ -89,7 +94,11 @@ protected LogEvent() /// A that represents this LogEvent. public override string ToString() { - return string.Format("[{0}][{1}][Thread {2}][{3}] {4}", LogLevel().PrettyNameFor(), Timestamp, Thread.ManagedThreadId.ToString().PadLeft(4, '0'), LogSource, Message); + if(Cause == null) + return + $"[{LogLevel().PrettyNameFor()}][{Timestamp}][Thread {Thread.ManagedThreadId.ToString().PadLeft(4, '0')}][{LogSource}] {Message}"; + return + $"[{LogLevel().PrettyNameFor()}][{Timestamp}][Thread {Thread.ManagedThreadId.ToString().PadLeft(4, '0')}][{LogSource}] {Message}{Environment.NewLine}Cause: {Cause}"; } } } diff --git a/src/core/Akka/Event/LoggingAdapterBase.cs b/src/core/Akka/Event/LoggingAdapterBase.cs index c1199f07034..08a1cb21f1b 100644 --- a/src/core/Akka/Event/LoggingAdapterBase.cs +++ b/src/core/Akka/Event/LoggingAdapterBase.cs @@ -135,7 +135,7 @@ protected void NotifyLog(LogLevel logLevel, object message) /// /// The message that is being logged. /// An optional list of items used to format the message. - public void Debug(string format, params object[] args) + public virtual void Debug(string format, params object[] args) { if (!IsDebugEnabled) return; @@ -150,22 +150,32 @@ public void Debug(string format, params object[] args) } } + public virtual void Debug(Exception cause, string format, params object[] args) + { + throw new NotImplementedException(); + } + /// /// Obsolete. Use instead! /// /// N/A /// N/A - public void Warn(string format, params object[] args) + public virtual void Warn(string format, params object[] args) { Warning(format, args); } + public virtual void Info(Exception cause, string format, params object[] args) + { + throw new NotImplementedException(); + } + /// /// Logs a message. /// /// The message that is being logged. /// An optional list of items used to format the message. - public void Warning(string format, params object[] args) + public virtual void Warning(string format, params object[] args) { if (!IsWarningEnabled) return; @@ -180,13 +190,18 @@ public void Warning(string format, params object[] args) } } + public virtual void Warning(Exception cause, string format, params object[] args) + { + throw new NotImplementedException(); + } + /// /// Logs a message and associated exception. /// /// The exception associated with this message. /// The message that is being logged. /// An optional list of items used to format the message. - public void Error(Exception cause, string format, params object[] args) + public virtual void Error(Exception cause, string format, params object[] args) { if (!IsErrorEnabled) return; @@ -206,7 +221,7 @@ public void Error(Exception cause, string format, params object[] args) /// /// The message that is being logged. /// An optional list of items used to format the message. - public void Error(string format, params object[] args) + public virtual void Error(string format, params object[] args) { if (!IsErrorEnabled) return; @@ -226,7 +241,7 @@ public void Error(string format, params object[] args) /// /// The message that is being logged. /// An optional list of items used to format the message. - public void Info(string format, params object[] args) + public virtual void Info(string format, params object[] args) { if (!IsInfoEnabled) return; @@ -247,7 +262,7 @@ public void Info(string format, params object[] args) /// The level used to log the message. /// The message that is being logged. /// An optional list of items used to format the message. - public void Log(LogLevel logLevel, string format, params object[] args) + public virtual void Log(LogLevel logLevel, string format, params object[] args) { if (args == null || args.Length == 0) { diff --git a/src/core/Akka/Event/Warning.cs b/src/core/Akka/Event/Warning.cs index 255b868c227..750a98beed6 100644 --- a/src/core/Akka/Event/Warning.cs +++ b/src/core/Akka/Event/Warning.cs @@ -21,10 +21,23 @@ public class Warning : LogEvent /// The type of logger used to log the event. /// The message that is being logged. public Warning(string logSource, Type logClass, object message) + : this(null, logSource, logClass, message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The exception that caused the log event. + /// The source that generated the log event. + /// The type of logger used to log the event. + /// The message that is being logged. + public Warning(Exception cause, string logSource, Type logClass, object message) { LogSource = logSource; LogClass = logClass; Message = message; + Cause = cause; } /// From f91bc8b3df589541670c43c6682d71e6899c8e85 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 21 Jun 2018 18:54:34 -0700 Subject: [PATCH 06/14] fixed issues with cause not being propagated / implemented --- src/core/Akka/Event/BusLogging.cs | 15 +++++ src/core/Akka/Event/LoggingAdapterBase.cs | 80 +++++++++++++++++++++-- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/core/Akka/Event/BusLogging.cs b/src/core/Akka/Event/BusLogging.cs index 750184d189b..3cc76772268 100644 --- a/src/core/Akka/Event/BusLogging.cs +++ b/src/core/Akka/Event/BusLogging.cs @@ -90,6 +90,11 @@ protected override void NotifyWarning(object message) _bus.Publish(new Warning(_logSource, _logClass, message)); } + protected override void NotifyWarning(Exception cause, object message) + { + _bus.Publish(new Warning(cause, _logSource, _logClass, message)); + } + /// /// Publishes the info message onto the LoggingBus. /// @@ -99,6 +104,11 @@ protected override void NotifyInfo(object message) _bus.Publish(new Info(_logSource, _logClass, message)); } + protected override void NotifyInfo(Exception cause, object message) + { + _bus.Publish(new Info(cause, _logSource, _logClass, message)); + } + /// /// Publishes the debug message onto the LoggingBus. /// @@ -107,5 +117,10 @@ protected override void NotifyDebug(object message) { _bus.Publish(new Debug(_logSource, _logClass, message)); } + + protected override void NotifyDebug(Exception cause, object message) + { + _bus.Publish(new Debug(cause, _logSource, _logClass, message)); + } } } diff --git a/src/core/Akka/Event/LoggingAdapterBase.cs b/src/core/Akka/Event/LoggingAdapterBase.cs index 08a1cb21f1b..b3c9be499c3 100644 --- a/src/core/Akka/Event/LoggingAdapterBase.cs +++ b/src/core/Akka/Event/LoggingAdapterBase.cs @@ -55,18 +55,39 @@ public abstract class LoggingAdapterBase : ILoggingAdapter /// The message related to the log event. protected abstract void NotifyWarning(object message); + /// + /// Notifies all subscribers that an log event occurred. + /// + /// The exception that caused the log event. + /// The message related to the log event. + protected abstract void NotifyWarning(Exception cause, object message); + /// /// Notifies all subscribers that an log event occurred. /// /// The message related to the log event. protected abstract void NotifyInfo(object message); + /// + /// Notifies all subscribers that an log event occurred. + /// + /// The exception that caused the log event. + /// The message related to the log event. + protected abstract void NotifyInfo(Exception cause, object message); + /// /// Notifies all subscribers that an log event occurred. /// /// The message related to the log event. protected abstract void NotifyDebug(object message); + /// + /// Notifies all subscribers that an log event occurred. + /// + /// The exception that caused the log event. + /// The message related to the log event. + protected abstract void NotifyDebug(Exception cause, object message); + /// /// Creates an instance of the LoggingAdapterBase. /// @@ -74,10 +95,7 @@ public abstract class LoggingAdapterBase : ILoggingAdapter /// This exception is thrown when the given is undefined. protected LoggingAdapterBase(ILogMessageFormatter logMessageFormatter) { - if(logMessageFormatter == null) - throw new ArgumentNullException(nameof(logMessageFormatter), "The message formatter must not be null."); - - _logMessageFormatter = logMessageFormatter; + _logMessageFormatter = logMessageFormatter ?? throw new ArgumentNullException(nameof(logMessageFormatter), "The message formatter must not be null."); } /// @@ -150,9 +168,25 @@ public virtual void Debug(string format, params object[] args) } } + /// + /// Logs a message. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. public virtual void Debug(Exception cause, string format, params object[] args) { - throw new NotImplementedException(); + if (!IsDebugEnabled) + return; + + if (args == null || args.Length == 0) + { + NotifyDebug(cause, format); + } + else + { + NotifyDebug(cause, new LogMessage(_logMessageFormatter, format, args)); + } } /// @@ -165,9 +199,25 @@ public virtual void Warn(string format, params object[] args) Warning(format, args); } + /// + /// Logs a message. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. public virtual void Info(Exception cause, string format, params object[] args) { - throw new NotImplementedException(); + if (!IsInfoEnabled) + return; + + if (args == null || args.Length == 0) + { + NotifyInfo(cause, format); + } + else + { + NotifyInfo(cause, new LogMessage(_logMessageFormatter, format, args)); + } } /// @@ -190,9 +240,25 @@ public virtual void Warning(string format, params object[] args) } } + /// + /// Logs a message. + /// + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. public virtual void Warning(Exception cause, string format, params object[] args) { - throw new NotImplementedException(); + if (!IsWarningEnabled) + return; + + if (args == null || args.Length == 0) + { + NotifyWarning(cause, format); + } + else + { + NotifyWarning(cause, new LogMessage(_logMessageFormatter, format, args)); + } } /// From 17ddf9292fed9f1cfbdf62ae3b63b00bb582bc68 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 21 Jun 2018 19:08:56 -0700 Subject: [PATCH 07/14] fully tested all new methods and combinations --- src/core/Akka.Tests/Event/LoggerSpec.cs | 34 +++++++++++++--- src/core/Akka/Event/ILoggingAdapter.cs | 20 ++++++++++ src/core/Akka/Event/LoggingAdapterBase.cs | 48 +++++++++++++++++++++++ 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/core/Akka.Tests/Event/LoggerSpec.cs b/src/core/Akka.Tests/Event/LoggerSpec.cs index eb8ab21be74..37bd008d2ae 100644 --- a/src/core/Akka.Tests/Event/LoggerSpec.cs +++ b/src/core/Akka.Tests/Event/LoggerSpec.cs @@ -47,7 +47,7 @@ public void LoggingAdapter_should_log_all_information(LogLevel logLevel, bool in { Sys.EventStream.Subscribe(TestActor, typeof(LogEvent)); var msg = args != null ? string.Format(formatStr, args) : formatStr; - var ex = new Exception(); + var ex = new Exception("errrrrrr"); switch (logLevel) { case LogLevel.DebugLevel when includeException: @@ -76,13 +76,35 @@ public void LoggingAdapter_should_log_all_information(LogLevel logLevel, bool in break; } - var log = ExpectMsg(); - log.Message.ToString().Should().Be(msg); - log.LogLevel().Should().Be(logLevel); + // log a second log message using the generic method if (includeException) - log.Cause.Should().Be(ex); + { + Log.Log(logLevel, ex, formatStr, args); + + } else - log.Cause.Should().BeNull(); + { + Log.Log(logLevel, formatStr, args); + } + + void ProcessLog(LogEvent logEvent) + { + logEvent.Message.ToString().Should().Be(msg); + logEvent.LogLevel().Should().Be(logLevel); + if (includeException) + { + logEvent.Cause.Should().Be(ex); + logEvent.ToString().Should().Contain(ex.Message); + } + else + logEvent.Cause.Should().BeNull(); + } + + var log = ExpectMsg(); + ProcessLog(log); + + var log2 = ExpectMsg(); + ProcessLog(log2); } [Fact] diff --git a/src/core/Akka/Event/ILoggingAdapter.cs b/src/core/Akka/Event/ILoggingAdapter.cs index d4fb8635709..beed716ad9f 100644 --- a/src/core/Akka/Event/ILoggingAdapter.cs +++ b/src/core/Akka/Event/ILoggingAdapter.cs @@ -108,6 +108,15 @@ public interface ILoggingAdapter /// The message that is being logged. /// An optional list of items used to format the message. void Log(LogLevel logLevel, string format, params object[] args); + + /// + /// Logs a message with a specified level. + /// + /// The level used to log the message. + /// The exception that caused this log message. + /// The message that is being logged. + /// An optional list of items used to format the message. + void Log(LogLevel logLevel, Exception cause, string format, params object[] args); } /// @@ -227,5 +236,16 @@ public void Error(Exception cause, string format, params object[] args) { } /// The message that is being logged. /// An optional list of items used to format the message. public void Log(LogLevel logLevel, string format, params object[] args) { } + + /// + /// Logs a message with a specified level. + /// + /// The level used to log the message. + /// The exception that caused this log message. + /// The message that is being logged. + /// An optional list of items used to format the message. + public void Log(LogLevel logLevel, Exception cause, string format, params object[] args) + { + } } } diff --git a/src/core/Akka/Event/LoggingAdapterBase.cs b/src/core/Akka/Event/LoggingAdapterBase.cs index b3c9be499c3..5517afdf5c3 100644 --- a/src/core/Akka/Event/LoggingAdapterBase.cs +++ b/src/core/Akka/Event/LoggingAdapterBase.cs @@ -148,6 +148,35 @@ protected void NotifyLog(LogLevel logLevel, object message) } } + /// + /// Notifies all subscribers that a log event occurred for a particular level. + /// + /// The log level associated with the log event. + /// The exception that caused the log event. + /// The message related to the log event. + /// This exception is thrown when the given is unknown. + protected void NotifyLog(LogLevel logLevel, Exception cause, object message) + { + switch (logLevel) + { + case LogLevel.DebugLevel: + if (IsDebugEnabled) NotifyDebug(cause, message); + break; + case LogLevel.InfoLevel: + if (IsInfoEnabled) NotifyInfo(cause, message); + break; + case LogLevel.WarningLevel: + if (IsWarningEnabled) NotifyWarning(cause, message); + break; + case LogLevel.ErrorLevel: + if (IsErrorEnabled) NotifyError(cause, message); + break; + default: + throw new NotSupportedException($"Unknown LogLevel {logLevel}"); + } + } + + /// /// Logs a message. /// @@ -339,5 +368,24 @@ public virtual void Log(LogLevel logLevel, string format, params object[] args) NotifyLog(logLevel, new LogMessage(_logMessageFormatter, format, args)); } } + + /// + /// Logs a message with a specified level. + /// + /// The level used to log the message. + /// The exception associated with this message. + /// The message that is being logged. + /// An optional list of items used to format the message. + public void Log(LogLevel logLevel, Exception cause, string format, params object[] args) + { + if (args == null || args.Length == 0) + { + NotifyLog(logLevel, cause, format); + } + else + { + NotifyLog(logLevel, cause, new LogMessage(_logMessageFormatter, format, args)); + } + } } } From 70f143eacee05fcf533a3017fc1377f180671924 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 21 Jun 2018 19:17:55 -0700 Subject: [PATCH 08/14] API approval --- .../CoreAPISpec.ApproveCore.approved.txt | 39 ++++++++++++++----- src/core/Akka/Event/LoggingAdapterBase.cs | 2 +- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index b2a78a7fe9c..c361fbbdba9 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -2811,10 +2811,13 @@ namespace Akka.Event public override bool IsInfoEnabled { get; } public override bool IsWarningEnabled { get; } protected override void NotifyDebug(object message) { } + protected override void NotifyDebug(System.Exception cause, object message) { } protected override void NotifyError(object message) { } protected override void NotifyError(System.Exception cause, object message) { } protected override void NotifyInfo(object message) { } + protected override void NotifyInfo(System.Exception cause, object message) { } protected override void NotifyWarning(object message) { } + protected override void NotifyWarning(System.Exception cause, object message) { } } public sealed class DeadLetter : Akka.Event.AllDeadLetters { @@ -2831,6 +2834,7 @@ namespace Akka.Event public class Debug : Akka.Event.LogEvent { public Debug(string logSource, System.Type logClass, object message) { } + public Debug(System.Exception cause, string logSource, System.Type logClass, object message) { } public override Akka.Event.LogLevel LogLevel() { } } public class DefaultLogger : Akka.Actor.ActorBase, Akka.Dispatch.IRequiresMessageQueue @@ -2851,9 +2855,7 @@ namespace Akka.Event public class Error : Akka.Event.LogEvent { public Error(System.Exception cause, string logSource, System.Type logClass, object message) { } - public System.Exception Cause { get; } public override Akka.Event.LogLevel LogLevel() { } - public override string ToString() { } } public abstract class EventBus { @@ -2892,12 +2894,16 @@ namespace Akka.Event bool IsInfoEnabled { get; } bool IsWarningEnabled { get; } void Debug(string format, params object[] args); + void Debug(System.Exception cause, string format, params object[] args); void Error(string format, params object[] args); void Error(System.Exception cause, string format, params object[] args); void Info(string format, params object[] args); + void Info(System.Exception cause, string format, params object[] args); bool IsEnabled(Akka.Event.LogLevel logLevel); void Log(Akka.Event.LogLevel logLevel, string format, params object[] args); + void Log(Akka.Event.LogLevel logLevel, System.Exception cause, string format, params object[] args); void Warning(string format, params object[] args); + void Warning(System.Exception cause, string format, params object[] args); } public interface ILogMessageFormatter { @@ -2906,6 +2912,7 @@ namespace Akka.Event public class Info : Akka.Event.LogEvent { public Info(string logSource, System.Type logClass, object message) { } + public Info(System.Exception cause, string logSource, System.Type logClass, object message) { } public override Akka.Event.LogLevel LogLevel() { } } public class InitializeLogger : Akka.Actor.INoSerializationVerificationNeeded @@ -2916,6 +2923,7 @@ namespace Akka.Event public abstract class LogEvent : Akka.Actor.INoSerializationVerificationNeeded { protected LogEvent() { } + public System.Exception Cause { get; set; } public System.Type LogClass { get; set; } public string LogSource { get; set; } public object Message { get; set; } @@ -2951,20 +2959,28 @@ namespace Akka.Event public abstract bool IsErrorEnabled { get; } public abstract bool IsInfoEnabled { get; } public abstract bool IsWarningEnabled { get; } - public void Debug(string format, params object[] args) { } - public void Error(System.Exception cause, string format, params object[] args) { } - public void Error(string format, params object[] args) { } - public void Info(string format, params object[] args) { } + public virtual void Debug(string format, params object[] args) { } + public virtual void Debug(System.Exception cause, string format, params object[] args) { } + public virtual void Error(System.Exception cause, string format, params object[] args) { } + public virtual void Error(string format, params object[] args) { } + public virtual void Info(System.Exception cause, string format, params object[] args) { } + public virtual void Info(string format, params object[] args) { } public bool IsEnabled(Akka.Event.LogLevel logLevel) { } - public void Log(Akka.Event.LogLevel logLevel, string format, params object[] args) { } + public virtual void Log(Akka.Event.LogLevel logLevel, string format, params object[] args) { } + public virtual void Log(Akka.Event.LogLevel logLevel, System.Exception cause, string format, params object[] args) { } protected abstract void NotifyDebug(object message); + protected abstract void NotifyDebug(System.Exception cause, object message); protected abstract void NotifyError(object message); protected abstract void NotifyError(System.Exception cause, object message); protected abstract void NotifyInfo(object message); + protected abstract void NotifyInfo(System.Exception cause, object message); protected void NotifyLog(Akka.Event.LogLevel logLevel, object message) { } + protected void NotifyLog(Akka.Event.LogLevel logLevel, System.Exception cause, object message) { } protected abstract void NotifyWarning(object message); - public void Warn(string format, params object[] args) { } - public void Warning(string format, params object[] args) { } + protected abstract void NotifyWarning(System.Exception cause, object message); + public virtual void Warn(string format, params object[] args) { } + public virtual void Warning(string format, params object[] args) { } + public virtual void Warning(System.Exception cause, string format, params object[] args) { } } public class LoggingBus : Akka.Event.ActorEventBus { @@ -2999,13 +3015,17 @@ namespace Akka.Event public bool IsInfoEnabled { get; } public bool IsWarningEnabled { get; } public void Debug(string format, params object[] args) { } + public void Debug(System.Exception cause, string format, params object[] args) { } public void Error(string format, params object[] args) { } public void Error(System.Exception cause, string format, params object[] args) { } public void Info(string format, params object[] args) { } + public void Info(System.Exception cause, string format, params object[] args) { } public bool IsEnabled(Akka.Event.LogLevel logLevel) { } public void Log(Akka.Event.LogLevel logLevel, string format, params object[] args) { } + public void Log(Akka.Event.LogLevel logLevel, System.Exception cause, string format, params object[] args) { } public void Warn(string format, params object[] args) { } public void Warning(string format, params object[] args) { } + public void Warning(System.Exception cause, string format, params object[] args) { } } public class StandardOutLogger : Akka.Actor.MinimalActorRef { @@ -3046,6 +3066,7 @@ namespace Akka.Event public class Warning : Akka.Event.LogEvent { public Warning(string logSource, System.Type logClass, object message) { } + public Warning(System.Exception cause, string logSource, System.Type logClass, object message) { } public override Akka.Event.LogLevel LogLevel() { } } } diff --git a/src/core/Akka/Event/LoggingAdapterBase.cs b/src/core/Akka/Event/LoggingAdapterBase.cs index 5517afdf5c3..e7f726eb663 100644 --- a/src/core/Akka/Event/LoggingAdapterBase.cs +++ b/src/core/Akka/Event/LoggingAdapterBase.cs @@ -376,7 +376,7 @@ public virtual void Log(LogLevel logLevel, string format, params object[] args) /// The exception associated with this message. /// The message that is being logged. /// An optional list of items used to format the message. - public void Log(LogLevel logLevel, Exception cause, string format, params object[] args) + public virtual void Log(LogLevel logLevel, Exception cause, string format, params object[] args) { if (args == null || args.Length == 0) { From 28ee736ec43574786ff4310d22ab41a5d69e4216 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Thu, 5 Jul 2018 22:31:57 +0200 Subject: [PATCH 09/14] reintroduced FunctionRefs --- .../Dsl/StageActorRefSpec.cs | 163 ++++------ .../ActorRefBackpressureSinkStage.cs | 16 +- .../Implementation/IO/TcpStages.cs | 50 +-- src/core/Akka.Streams/Stage/GraphStage.cs | 307 ++++++------------ src/core/Akka.Tests/Actor/FunctionRefSpecs.cs | 255 +++++++++++++++ src/core/Akka/Actor/ActorCell.Children.cs | 68 +++- .../Akka/Actor/ActorCell.FaultHandling.cs | 20 +- src/core/Akka/Actor/ActorRef.cs | 203 +++++++++++- src/core/Akka/Actor/IAutoReceivedMessage.cs | 23 +- src/core/Akka/Actor/RepointableActorRef.cs | 9 +- src/core/Akka/Util/Base64Encoding.cs | 14 +- 11 files changed, 750 insertions(+), 378 deletions(-) create mode 100644 src/core/Akka.Tests/Actor/FunctionRefSpecs.cs diff --git a/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs b/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs index 8180339e889..e119aeb74b4 100644 --- a/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/StageActorRefSpec.cs @@ -1,10 +1,4 @@ -//----------------------------------------------------------------------- -// -// Copyright (C) 2009-2018 Lightbend Inc. -// Copyright (C) 2013-2018 .NET Foundation -// -//----------------------------------------------------------------------- - + using System; using System.Threading.Tasks; using Akka.Actor; @@ -34,26 +28,23 @@ private static GraphStageWithMaterializedValue, Task> SumSta => new SumTestStage(probe); [Fact] - public void A_Graph_stage_ActorRef_must_receive_messages() + public async Task A_Graph_stage_ActorRef_must_receive_messages() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); - var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new Add(1)); stageRef.Tell(new Add(2)); stageRef.Tell(new Add(3)); - stageRef.Tell(StopNow.Instance); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(6); + + (await t.Item2).Should().Be(6); } [Fact] - public void A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() + public async Task A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); - var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new AddAndTell(1)); @@ -63,15 +54,13 @@ public void A_Graph_stage_ActorRef_must_be_able_to_be_replied_to() ExpectMsg(10); stageRef.Tell(StopNow.Instance); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(10); + (await t.Item2).Should().Be(10); } [Fact] - public void A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time() + public async Task A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); - var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(CallInitStageActorRef.Instance); @@ -85,16 +74,15 @@ public void A_Graph_stage_ActorRef_must_yield_the_same_self_ref_each_time() ExpectMsg(6); stageRef.Tell(StopNow.Instance); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(6); + + (await t.Item2).Should().Be(6); } [Fact] - public void A_Graph_stage_ActorRef_must_be_watchable() + public async Task A_Graph_stage_ActorRef_must_be_watchable() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); @@ -102,17 +90,15 @@ public void A_Graph_stage_ActorRef_must_be_watchable() stageRef.Tell(new Add(1)); source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + (await t.Item2).Should().Be(1); ExpectTerminated(stageRef); } [Fact] - public void A_Graph_stage_ActorRef_must_be_able_to_become() + public async Task A_Graph_stage_ActorRef_must_be_able_to_become() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); @@ -123,24 +109,24 @@ public void A_Graph_stage_ActorRef_must_be_able_to_become() ExpectMsg("42"); source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + (await t.Item2).Should().Be(1); + ExpectTerminated(stageRef); } [Fact] - public void A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_stage_is_watched() + public async Task A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_stage_is_watched() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); - stageRef.Tell(new Add(1)); + stageRef.Tell(new AddAndTell(1)); + ExpectMsg(1); source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + + (await t.Item2).Should().Be(1); ExpectTerminated(stageRef); var p = CreateTestProbe(); @@ -149,36 +135,35 @@ public void A_Graph_stage_ActorRef_must_reply_Terminated_when_terminated_stage_i } [Fact] - public void A_Graph_stage_ActorRef_must_be_unwatchable() + public async Task A_Graph_stage_ActorRef_must_be_unwatchable() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); Watch(stageRef); Unwatch(stageRef); - stageRef.Tell(new Add(1)); + stageRef.Tell(new AddAndTell(1)); + ExpectMsg(1); source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + + (await t.Item2).Should().Be(1); ExpectNoMsg(100); } [Fact] - public void A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_and_Kill_messages() + public async Task A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_and_Kill_messages() { var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); var source = t.Item1; - var res = t.Item2; var stageRef = ExpectMsg(); stageRef.Tell(new Add(40)); - + Sys.EventStream.Publish(new Mute(new CustomEventFilter(e => e is Warning))); - Sys.EventStream.Subscribe(TestActor, typeof (Warning)); + Sys.EventStream.Subscribe(TestActor, typeof(Warning)); stageRef.Tell(PoisonPill.Instance); var warn = ExpectMsg(TimeSpan.FromSeconds(1)); @@ -187,12 +172,12 @@ public void A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_a warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActorRef\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActor\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #else warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActorRef\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActor\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #endif stageRef.Tell(Kill.Instance); warn = ExpectMsg(TimeSpan.FromSeconds(1)); @@ -201,36 +186,17 @@ public void A_Graph_stage_ActorRef_must_ignore_and_log_warnings_for_PoisonPill_a warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActorRef\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActor\\(akka\\://AkkaSpec/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #else warn.Message.ToString() .Should() .MatchRegex( - " message sent to StageActorRef\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/StageActorRef-[0-9]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); + " message sent to StageActor\\(akka\\://StageActorRefSpec-[0-9]+/user/StreamSupervisor-[0-9]+/\\$\\$[a-z]+\\) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); #endif source.SetResult(2); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(42); - } - - [Fact] - public void A_Graph_stage_ActorRef_must_be_able_to_watch_other_actors() - { - var killMe = ActorOf(dsl => { }, "KilMe"); - var t = Source.Maybe().ToMaterialized(SumStage(TestActor), Keep.Both).Run(Materializer); - var source = t.Item1; - var res = t.Item2; - - var stageRef = ExpectMsg(); - stageRef.Tell(new WatchMe(killMe)); - stageRef.Tell(new Add(1)); - killMe.Tell(PoisonPill.Instance); - ExpectMsg().Watchee.Should().Be(killMe); - source.SetResult(0); - res.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue(); - res.Result.Should().Be(1); + (await t.Item2).Should().Be(42); } private sealed class Add @@ -271,37 +237,19 @@ private sealed class StopNow public static readonly StopNow Instance = new StopNow(); private StopNow() { } } - private sealed class WatchMe - { - public WatchMe(IActorRef watchee) - { - Watchee = watchee; - } - - public IActorRef Watchee { get; } - } - private sealed class WatcheeTerminated - { - public WatcheeTerminated(IActorRef watchee) - { - Watchee = watchee; - } - - public IActorRef Watchee { get; } - } private class SumTestStage : GraphStageWithMaterializedValue, Task> { private readonly IActorRef _probe; -#region internal classes + #region internal classes private class Logic : GraphStageLogic { private readonly SumTestStage _stage; private readonly TaskCompletionSource _promise; private int _sum; - private StageActorRef _self; + private IActorRef Self => StageActor.Ref; public Logic(SumTestStage stage, TaskCompletionSource promise) : base(stage.Shape) { @@ -327,8 +275,8 @@ public Logic(SumTestStage stage, TaskCompletionSource promise) : base(stage public override void PreStart() { Pull(_stage._inlet); - _self = GetStageActorRef(Behaviour); - _stage._probe.Tell(_self); + GetStageActor(Behaviour); + _stage._probe.Tell(Self); } private void Behaviour(Tuple args) @@ -336,28 +284,33 @@ private void Behaviour(Tuple args) var msg = args.Item2; var sender = args.Item1; - msg.Match() - .With(a => _sum += a.N) - .With(() => Pull(_stage._inlet)) - .With(() => sender.Tell(GetStageActorRef(Behaviour), _self)) - .With(() => GetStageActorRef(tuple => tuple.Item1.Tell(tuple.Item2.ToString()))) - .With(() => - { + switch (msg) + { + case Add add: _sum += add.N; break; + case PullNow _: Pull(_stage._inlet); break; + case CallInitStageActorRef _: sender.Tell(GetStageActor(Behaviour).Ref, Self); break; + case BecomeStringEcho _: + GetStageActor(tuple => + { + var theSender = tuple.Item1; + var theMsg = tuple.Item2; + theSender.Tell(theMsg.ToString(), Self); + }); break; + case StopNow _: _promise.TrySetResult(_sum); CompleteStage(); - }).With(a => - { - _sum += a.N; - sender.Tell(_sum, _self); - }) - .With(w => _self.Watch(w.Watchee)) - .With(t => _stage._probe.Tell(new WatcheeTerminated(t.ActorRef))); + break; + case AddAndTell addAndTell: + _sum += addAndTell.N; + sender.Tell(_sum, Self); + break; + } } } -#endregion + #endregion private readonly Inlet _inlet = new Inlet("IntSum.in"); - + public SumTestStage(IActorRef probe) { _probe = probe; @@ -373,4 +326,4 @@ public override ILogicAndMaterializedValue> CreateLogicAndMaterialized } } } -} +} \ No newline at end of file diff --git a/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs b/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs index 9f726e54ee6..0ae226d05fe 100644 --- a/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs +++ b/src/core/Akka.Streams/Implementation/ActorRefBackpressureSinkStage.cs @@ -30,7 +30,8 @@ private sealed class Logic : InGraphStageLogic private readonly int _maxBuffer; private readonly List _buffer; private readonly Type _ackType; - private StageActorRef _self; + + public IActorRef Self => StageActor.Ref; public Logic(ActorRefBackpressureSinkStage stage, int maxBuffer) : base(stage.Shape) { @@ -65,7 +66,7 @@ public override void OnUpstreamFinish() public override void OnUpstreamFailure(Exception ex) { - _stage._actorRef.Tell(_stage._onFailureMessage(ex), _self); + _stage._actorRef.Tell(_stage._onFailureMessage(ex), Self); _completionSignalled = true; FailStage(ex); } @@ -100,9 +101,8 @@ private void Receive(Tuple evt) public override void PreStart() { SetKeepGoing(true); - _self = GetStageActorRef(Receive); - _self.Watch(_stage._actorRef); - _stage._actorRef.Tell(_stage._onInitMessage, _self); + GetStageActor(Receive).Watch(_stage._actorRef); + _stage._actorRef.Tell(_stage._onInitMessage, Self); Pull(_stage._inlet); } @@ -110,14 +110,14 @@ private void DequeueAndSend() { var msg = _buffer[0]; _buffer.RemoveAt(0); - _stage._actorRef.Tell(msg, _self); + _stage._actorRef.Tell(msg, Self); if (_buffer.Count == 0 && _completeReceived) Finish(); } private void Finish() { - _stage._actorRef.Tell(_stage._onCompleteMessage, _self); + _stage._actorRef.Tell(_stage._onCompleteMessage, Self); _completionSignalled = true; CompleteStage(); } @@ -125,7 +125,7 @@ private void Finish() public override void PostStop() { if(!_completionSignalled) - StageActorRef.Tell(_stage._onFailureMessage(new AbruptStageTerminationException(this))); + Self.Tell(_stage._onFailureMessage(new AbruptStageTerminationException(this))); } public override string ToString() => "ActorRefBackpressureSink"; diff --git a/src/core/Akka.Streams/Implementation/IO/TcpStages.cs b/src/core/Akka.Streams/Implementation/IO/TcpStages.cs index c6cf0ca7e5c..3a38ed0af35 100644 --- a/src/core/Akka.Streams/Implementation/IO/TcpStages.cs +++ b/src/core/Akka.Streams/Implementation/IO/TcpStages.cs @@ -53,7 +53,7 @@ public ConnectionSourceStageLogic(Shape shape, ConnectionSourceStage stage, Task public void OnPull() { // Ignore if still binding - _listener?.Tell(new Tcp.ResumeAccepting(1), StageActorRef); + _listener?.Tell(new Tcp.ResumeAccepting(1), StageActor.Ref); } public void OnDownstreamFinish() => TryUnbind(); @@ -85,13 +85,13 @@ private void TryUnbind() { _unbindStarted = true; SetKeepGoing(true); - _listener.Tell(Tcp.Unbind.Instance, StageActorRef); + _listener.Tell(Tcp.Unbind.Instance, StageActor.Ref); } } private void UnbindCompleted() { - StageActorRef.Unwatch(_listener); + StageActor.Unwatch(_listener); if (_connectionFlowsAwaitingInitialization.Current == 0) CompleteStage(); else @@ -106,8 +106,8 @@ protected internal override void OnTimer(object timerKey) public override void PreStart() { - GetStageActorRef(Receive); - _stage._tcpManager.Tell(new Tcp.Bind(StageActorRef, _stage._endpoint, _stage._backlog, _stage._options, pullMode: true), StageActorRef); + GetStageActor(Receive); + _stage._tcpManager.Tell(new Tcp.Bind(StageActor.Ref, _stage._endpoint, _stage._backlog, _stage._options, pullMode: true), StageActor.Ref); } private void Receive(Tuple args) @@ -118,12 +118,12 @@ private void Receive(Tuple args) { var bound = (Tcp.Bound)msg; _listener = sender; - StageActorRef.Watch(_listener); + StageActor.Watch(_listener); if (IsAvailable(_stage._out)) - _listener.Tell(new Tcp.ResumeAccepting(1), StageActorRef); + _listener.Tell(new Tcp.ResumeAccepting(1), StageActor.Ref); - var thisStage = StageActorRef; + var thisStage = StageActor.Ref; var binding = new StreamTcp.ServerBinding(bound.LocalAddress, () => { // Beware, sender must be explicit since stageActor.ref will be invalid to access after the stage stopped @@ -406,14 +406,14 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En _bytesOut = shape.Outlet; _readHandler = new LambdaOutHandler( - onPull: () => _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef), + onPull: () => _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref), onDownstreamFinish: () => { if (!IsClosed(_bytesIn)) - _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); + _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); else { - _connection.Tell(Tcp.Abort.Instance, StageActorRef); + _connection.Tell(Tcp.Abort.Instance, StageActor.Ref); CompleteStage(); } }); @@ -425,17 +425,17 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En { var elem = Grab(_bytesIn); ReactiveStreamsCompliance.RequireNonNullElement(elem); - _connection.Tell(Tcp.Write.Create(elem, WriteAck.Instance), StageActorRef); + _connection.Tell(Tcp.Write.Create(elem, WriteAck.Instance), StageActor.Ref); }, onUpstreamFinish: () => { // Reading has stopped before, either because of cancel, or PeerClosed, so just Close now // (or half-close is turned off) if (IsClosed(_bytesOut) || !_role.HalfClose) - _connection.Tell(Tcp.Close.Instance, StageActorRef); + _connection.Tell(Tcp.Close.Instance, StageActor.Ref); // We still read, so we only close the write side else if (_connection != null) - _connection.Tell(Tcp.ConfirmedClose.Instance, StageActorRef); + _connection.Tell(Tcp.ConfirmedClose.Instance, StageActor.Ref); else CompleteStage(); }, @@ -446,7 +446,7 @@ public TcpStreamLogic(FlowShape shape, ITcpRole role, En if (Interpreter.Log.IsDebugEnabled) Interpreter.Log.Debug( $"Aborting tcp connection to {_remoteAddress} because of upstream failure: {ex.Message}\n{ex.StackTrace}"); - _connection.Tell(Tcp.Abort.Instance, StageActorRef); + _connection.Tell(Tcp.Abort.Instance, StageActor.Ref); } else FailStage(ex); @@ -465,15 +465,15 @@ public override void PreStart() var inbound = (Inbound)_role; SetHandler(_bytesOut, _readHandler); _connection = inbound.Connection; - GetStageActorRef(Connected).Watch(_connection); - _connection.Tell(new Tcp.Register(StageActorRef, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActorRef); + GetStageActor(Connected).Watch(_connection); + _connection.Tell(new Tcp.Register(StageActor.Ref, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActor.Ref); Pull(_bytesIn); } else { var outbound = (Outbound)_role; - GetStageActorRef(Connecting(outbound)).Watch(outbound.Manager); - outbound.Manager.Tell(outbound.ConnectCmd, StageActorRef); + GetStageActor(Connecting(outbound)).Watch(outbound.Manager); + outbound.Manager.Tell(outbound.ConnectCmd, StageActor.Ref); } } @@ -508,13 +508,13 @@ private StageActorRef.Receive Connecting(Outbound outbound) ((Outbound)_role).LocalAddressPromise.TrySetResult(connected.LocalAddress); _connection = sender; SetHandler(_bytesOut, _readHandler); - StageActorRef.Unwatch(outbound.Manager); - StageActorRef.Become(Connected); - StageActorRef.Watch(_connection); - _connection.Tell(new Tcp.Register(StageActorRef, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActorRef); + StageActor.Unwatch(outbound.Manager); + StageActor.Become(Connected); + StageActor.Watch(_connection); + _connection.Tell(new Tcp.Register(StageActor.Ref, keepOpenOnPeerClosed: true, useResumeWriting: false), StageActor.Ref); if (IsAvailable(_bytesOut)) - _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); + _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); Pull(_bytesIn); } @@ -537,7 +537,7 @@ private void Connected(Tuple args) { var received = (Tcp.Received)msg; // Keep on reading even when closed. There is no "close-read-side" in TCP - if (IsClosed(_bytesOut)) _connection.Tell(Tcp.ResumeReading.Instance, StageActorRef); + if (IsClosed(_bytesOut)) _connection.Tell(Tcp.ResumeReading.Instance, StageActor.Ref); else Push(_bytesOut, received.Data); } else if (msg is WriteAck) diff --git a/src/core/Akka.Streams/Stage/GraphStage.cs b/src/core/Akka.Streams/Stage/GraphStage.cs index 08613227e97..60845240f23 100644 --- a/src/core/Akka.Streams/Stage/GraphStage.cs +++ b/src/core/Akka.Streams/Stage/GraphStage.cs @@ -127,7 +127,7 @@ protected GraphStageWithMaterializedValue() new Lazy( () => new GraphStageModule(Shape, InitialAttributes, - (IGraphStageWithMaterializedValue) this)); + (IGraphStageWithMaterializedValue)this)); } /// @@ -449,7 +449,7 @@ public override void OnPush() var element = _logic.Grab(_inlet); _n--; - if(_n > 0) + if (_n > 0) _logic.Pull(_inlet); else _logic.SetHandler(_inlet, Previous); @@ -498,7 +498,7 @@ protected void FollowUp() // If (while executing andThen() callback) handler was changed to new emitting, // we should add it to the end of emission queue var currentHandler = Logic.GetHandler(Out); - if(currentHandler is Emitting e) + if (currentHandler is Emitting e) AddFollowUp(e); var next = Dequeue(); @@ -859,18 +859,18 @@ protected GraphStageLogic(Shape shape) : this(shape.Inlets.Count(), shape.Outlet /// public virtual bool KeepGoingAfterAllPortsClosed => false; - private StageActorRef _stageActorRef; + private StageActor _stageActor; /// /// TBD /// - public StageActorRef StageActorRef + public StageActor StageActor { get { - if (_stageActorRef == null) + if (_stageActor == null) throw StageActorRefNotInitializedException.Instance; - return _stageActorRef; + return _stageActor; } } @@ -1147,7 +1147,7 @@ private bool IsAvailable(Inlet inlet) // fast path return !ReferenceEquals(connection.Slot, Empty.Instance); } - + // slow path on failure if ((connection.PortState & (InReady | InFailed)) == (InReady | InFailed)) { @@ -1229,7 +1229,7 @@ protected internal void Push(Outlet outlet, T element) /// /// TBD protected void SetKeepGoing(bool enabled) => Interpreter.SetKeepGoing(this, enabled); - + /// /// Signals that there will be no more elements emitted on the given port. /// @@ -1300,7 +1300,7 @@ public void FailStage(Exception reason) /// /// TBD /// TBD - protected internal bool IsAvailable(Outlet outlet) + protected internal bool IsAvailable(Outlet outlet) => (GetConnection(outlet).PortState & (OutReady | OutClosed)) == OutReady; /// @@ -1308,7 +1308,7 @@ protected internal bool IsAvailable(Outlet outlet) /// /// TBD /// TBD - protected bool IsClosed(Outlet outlet) + protected bool IsClosed(Outlet outlet) => (GetConnection(outlet).PortState & OutClosed) != 0; /// @@ -1586,7 +1586,7 @@ protected void PassAlong(Inlet from, Outlet to, bool doFin /// TBD /// TBD protected Action GetAsyncCallback(Action handler) - => @event => Interpreter.OnAsyncInput(this, @event, x => handler((T) x)); + => @event => Interpreter.OnAsyncInput(this, @event, x => handler((T)x)); /// /// Obtain a callback object that can be used asynchronously to re-enter the @@ -1619,21 +1619,35 @@ protected Action GetAsyncCallback(Action handler) /// Callback that will be called upon receiving of a message by this special Actor /// Minimal actor with watch method [ApiMayChange] - protected StageActorRef GetStageActorRef(StageActorRef.Receive receive) + protected StageActor GetStageActor(StageActorRef.Receive receive) { - if (_stageActorRef == null) + if (_stageActor == null) { var actorMaterializer = ActorMaterializerHelper.Downcast(Interpreter.Materializer); - var provider = ((IInternalActorRef)actorMaterializer.Supervisor).Provider; - var path = actorMaterializer.Supervisor.Path / StageActorRef.Name.Next(); - _stageActorRef = new StageActorRef(provider, actorMaterializer.Logger, r => GetAsyncCallback>(tuple => r(tuple)), receive, path); + _stageActor = new StageActor( + actorMaterializer, + r => GetAsyncCallback>(message => r(message)), + receive, + StageActorName); } else - _stageActorRef.Become(receive); + _stageActor.Become(receive); - return _stageActorRef; + return _stageActor; } + /// + /// Override and return a name to be given to the StageActor of this stage. + /// + /// This method will be only invoked and used once, during the first + /// invocation whichc reates the actor, since subsequent `getStageActors` calls function + /// like `become`, rather than creating new actors. + /// + /// Returns an empty string by default, which means that the name will a unique generated String (e.g. "$$a"). + /// + [ApiMayChange] + protected virtual string StageActorName => ""; + /// /// TBD /// @@ -1644,10 +1658,10 @@ protected internal virtual void BeforePreStart() { } /// protected internal virtual void AfterPostStop() { - if (_stageActorRef != null) + if (_stageActor != null) { - _stageActorRef.Stop(); - _stageActorRef = null; + _stageActor.Stop(); + _stageActor = null; } } @@ -2065,7 +2079,7 @@ protected InGraphStageLogic(Shape shape) : base(shape) /// Called when the input port is finished. After this callback no other callbacks will be called for this port. /// public virtual void OnUpstreamFinish() => CompleteStage(); - + /// /// Called when the input port has failed. After this callback no other callbacks will be called for this port. /// @@ -2141,7 +2155,7 @@ protected InAndOutGraphStageLogic(Shape shape) : base(shape) /// Called when the input port has a new element available. The actual element can be retrieved via the method. /// public abstract void OnPush(); - + /// /// Called when the input port is finished. After this callback no other callbacks will be called for this port. /// @@ -2349,222 +2363,94 @@ public override void OnDownstreamFinish() } } + public static class StageActorRef + { + public delegate void Receive(Tuple args); + } + /// /// Minimal actor to work with other actors and watch them in a synchronous ways. /// - public sealed class StageActorRef : MinimalActorRef + public sealed class StageActor { - /// - /// TBD - /// - /// TBD - public delegate void Receive(Tuple args); - - /// - /// TBD - /// - public readonly IImmutableSet StageTerminatedTombstone = null; - - /// - /// TBD - /// - public static readonly EnumerableActorName Name = new EnumerableActorNameImpl("StageActorRef", new AtomicCounterLong(0L)); - - /// - /// TBD - /// - public readonly ILoggingAdapter Log; private readonly Action> _callback; - private readonly AtomicReference> _watchedBy = new AtomicReference>(ImmutableHashSet.Empty); + private readonly ActorCell _cell; + private readonly FunctionRef _functionRef; + private StageActorRef.Receive _behavior; - private volatile Receive _behavior; - private IImmutableSet _watching = ImmutableHashSet.Empty; - - /// - /// TBD - /// - /// TBD - /// TBD - /// TBD - /// TBD - /// TBD - /// TBD - public StageActorRef(IActorRefProvider provider, ILoggingAdapter log, Func>> getAsyncCallback, Receive initialReceive, ActorPath path) + public StageActor( + ActorMaterializer materializer, + Func>> getAsyncCallback, + StageActorRef.Receive initialReceive, + string name = null) { - Log = log; - Provider = provider; + _callback = getAsyncCallback(InternalReceive); _behavior = initialReceive; - Path = path; - - _callback = getAsyncCallback(args => _behavior(args)); - } - /// - /// TBD - /// - public override ActorPath Path { get; } - - /// - /// TBD - /// - public override IActorRefProvider Provider { get; } - - /// - /// TBD - /// - public override bool IsTerminated => _watchedBy.Value == StageTerminatedTombstone; + switch (materializer.Supervisor) + { + case LocalActorRef r: _cell = r.Cell; break; + case RepointableActorRef r: _cell = (ActorCell)r.Underlying; break; + default: throw new IllegalStateException($"Stream supervisor must be a local actor, was [{materializer.Supervisor.GetType()}]"); + } - private void LogIgnored(object message) => Log.Warning($"{message} message sent to StageActorRef({Path}) will be ignored, since it is not a real Actor. Use a custom message type to communicate with it instead."); - /// - /// TBD - /// - /// TBD - /// TBD - protected override void TellInternal(object message, IActorRef sender) - { - switch (message) + _functionRef = _cell.AddFunctionRef((sender, message) => { - case PoisonPill _: - case Kill _: - LogIgnored(message); - return; - case Terminated t: - if (_watching.Contains(t.ActorRef)) - { - _watching.Remove(t.ActorRef); - _callback(Tuple.Create(sender, message)); + switch (message) + { + case PoisonPill _: + case Kill _: + materializer.Logger.Warning("{0} message sent to StageActor({1}) will be ignored, since it is not a real Actor. " + + "Use a custom message type to communicate with it instead.", message, _functionRef.Path); break; - } - else return; - default: - _callback(Tuple.Create(sender, message)); - break; - } + default: _callback(Tuple.Create(sender, message)); break; + } + }); } /// - /// TBD + /// The by which this can be contacted from the outside. + /// This is a full-fledged that supports watching and being watched + /// as well as location transparent (remote) communication. /// - /// TBD - public override void SendSystemMessage(ISystemMessage message) - { - if (message is DeathWatchNotification death) - Tell(new Terminated(death.Actor, true, false), ActorRefs.NoSender); - else if (message is Watch w) - AddWatcher(w.Watchee, w.Watcher); - else if (message is Unwatch u) - RemoveWatcher(u.Watchee, u.Watcher); - } + public IActorRef Ref => _functionRef; /// - /// TBD + /// Special `Become` allowing to swap the behaviour of this . + /// Unbecome is not available. /// - /// TBD - public void Become(Receive behavior) => _behavior = behavior; - - private void SendTerminated() - { - var watchedBy = _watchedBy.GetAndSet(StageTerminatedTombstone); - if (watchedBy != StageTerminatedTombstone) - { - foreach (var actorRef in watchedBy.Cast()) - SendTerminated(actorRef); - - foreach (var actorRef in _watching.Cast()) - UnwatchWatched(actorRef); - - _watching = ImmutableHashSet.Empty; - } - } + public void Become(StageActorRef.Receive receive) => Volatile.Write(ref _behavior, receive); /// - /// TBD + /// Stops current . /// - /// TBD - public void Watch(IActorRef actorRef) - { - var iw = (IInternalActorRef) actorRef; - _watching = _watching.Add(actorRef); - iw.SendSystemMessage(new Watch(iw, this)); - } + public void Stop() => _cell.RemoveFunctionRef(_functionRef); /// - /// TBD + /// Makes current watch over given . + /// It will be notified when an underlying actor is . /// - /// TBD - public void Unwatch(IActorRef actorRef) - { - var iw = (IInternalActorRef)actorRef; - _watching = _watching.Remove(actorRef); - iw.SendSystemMessage(new Unwatch(iw, this)); - } + /// + public void Watch(IActorRef actorRef) => _functionRef.Watch(actorRef); /// - /// TBD + /// Makes current stop watching previously ed . + /// If was not watched over, this method has no result. /// - public override void Stop() => SendTerminated(); - - private void SendTerminated(IInternalActorRef actorRef) - => actorRef.SendSystemMessage(new DeathWatchNotification(this, true, false)); - - private void UnwatchWatched(IInternalActorRef actorRef) => actorRef.SendSystemMessage(new Unwatch(actorRef, this)); + /// + public void Unwatch(IActorRef actorRef) => _functionRef.Unwatch(actorRef); - private void AddWatcher(IInternalActorRef watchee, IInternalActorRef watcher) + internal void InternalReceive(Tuple pack) { - while (true) + if (pack.Item2 is Terminated terminated) { - var watchedBy = _watchedBy.Value; - if (watchedBy == StageTerminatedTombstone) - SendTerminated(watcher); - else + if (_functionRef.IsWatching(terminated.ActorRef)) { - var isWatcheeSelf = Equals(watchee, this); - var isWatcherSelf = Equals(watcher, this); - - if (isWatcheeSelf && !isWatcherSelf) - { - if (!watchedBy.Contains(watcher)) - if (!_watchedBy.CompareAndSet(watchedBy, watchedBy.Add(watcher))) - continue; // try again - } - else if (!isWatcheeSelf && isWatcherSelf) - Log.Warning("externally triggered watch from {0} to {1} is illegal on StageActorRef", - watcher, watchee); - else - Log.Error("BUG: illegal Watch({0}, {1}) for {2}", watchee, watcher, this); + _functionRef.Unwatch(terminated.ActorRef); + _behavior(pack); } - - break; - } - } - - private void RemoveWatcher(IInternalActorRef watchee, IInternalActorRef watcher) - { - while (true) - { - var watchedBy = _watchedBy.Value; - if (watchedBy == null) - SendTerminated(watcher); - else - { - var isWatcheeSelf = Equals(watchee, this); - var isWatcherSelf = Equals(watcher, this); - - if (isWatcheeSelf && !isWatcherSelf) - { - if (!watchedBy.Contains(watcher)) - if (!_watchedBy.CompareAndSet(watchedBy, watchedBy.Remove(watcher))) - continue; // try again - } - else if (!isWatcheeSelf && isWatcherSelf) - Log.Warning("externally triggered unwatch from {0} to {1} is illegal on StageActorRef", - watcher, watchee); - else - Log.Error("BUG: illegal Watch({0}, {1}) for {2}", watchee, watcher, this); - } - - break; } + else _behavior(pack); } } @@ -2661,15 +2547,10 @@ protected void InvokeCallbacks(T arg) => Locked(() => private void Locked(Action body) { - Monitor.Enter(this); - try + lock (this) { body(); } - finally - { - Monitor.Exit(this); - } } } -} +} \ No newline at end of file diff --git a/src/core/Akka.Tests/Actor/FunctionRefSpecs.cs b/src/core/Akka.Tests/Actor/FunctionRefSpecs.cs new file mode 100644 index 00000000000..a4cc01e2d38 --- /dev/null +++ b/src/core/Akka.Tests/Actor/FunctionRefSpecs.cs @@ -0,0 +1,255 @@ +#region copyright +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- +#endregion + +using System; +using Akka.Actor; +using Akka.Configuration; +using Akka.TestKit; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Tests.Actor +{ + public class FunctionRefSpec : AkkaSpec + { + #region internal classes + + sealed class GetForwarder : IEquatable + { + public IActorRef ReplyTo { get; } + + public GetForwarder(IActorRef replyTo) + { + ReplyTo = replyTo; + } + + public bool Equals(GetForwarder other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(ReplyTo, other.ReplyTo); + } + + public override bool Equals(object obj) => obj is GetForwarder forwarder && Equals(forwarder); + + public override int GetHashCode() => (ReplyTo != null ? ReplyTo.GetHashCode() : 0); + } + + sealed class DropForwarder : IEquatable + { + public FunctionRef Ref { get; } + + public DropForwarder(FunctionRef @ref) + { + Ref = @ref; + } + + public bool Equals(DropForwarder other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Ref, other.Ref); + } + + public override bool Equals(object obj) => obj is DropForwarder forwarder && Equals(forwarder); + + public override int GetHashCode() => (Ref != null ? Ref.GetHashCode() : 0); + } + + sealed class Forwarded : IEquatable + { + public object Message { get; } + public IActorRef Sender { get; } + + public Forwarded(object message, IActorRef sender) + { + Message = message; + Sender = sender; + } + + public bool Equals(Forwarded other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(Message, other.Message) && Equals(Sender, other.Sender); + } + + public override bool Equals(object obj) => obj is Forwarded forwarded && Equals(forwarded); + + public override int GetHashCode() + { + unchecked + { + return ((Message != null ? Message.GetHashCode() : 0) * 397) ^ (Sender != null ? Sender.GetHashCode() : 0); + } + } + } + + sealed class Super : ReceiveActor + { + public Super() + { + Receive(get => + { + var cell = (ActorCell)Context; + var fref = cell.AddFunctionRef((sender, msg) => + { + get.ReplyTo.Tell(new Forwarded(msg, sender)); + }); + get.ReplyTo.Tell(fref); + }); + Receive(drop => { + var cell = (ActorCell)Context; + cell.RemoveFunctionRef(drop.Ref); + }); + } + } + + sealed class SupSuper : ReceiveActor + { + public SupSuper() + { + var s = Context.ActorOf(Props.Create(), "super"); + ReceiveAny(msg => s.Tell(msg)); + } + } + + #endregion + + public FunctionRefSpec(ITestOutputHelper output) : base(output, null) + { + } + + #region top level + + [Fact] + public void FunctionRef_created_by_top_level_actor_must_forward_messages() + { + var s = SuperActor(); + var forwarder = GetFunctionRef(s); + + forwarder.Tell("hello"); + ExpectMsg(new Forwarded("hello", TestActor)); + } + + [Fact] + public void FunctionRef_created_by_top_level_actor_must_be_watchable() + { + var s = SuperActor(); + var forwarder = GetFunctionRef(s); + + s.Tell(new GetForwarder(TestActor)); + var f = ExpectMsg(); + Watch(f); + s.Tell(new DropForwarder(f)); + ExpectTerminated(f); + } + + [Fact] + public void FunctionRef_created_by_top_level_actor_must_be_able_to_watch() + { + var s = SuperActor(); + var forwarder = GetFunctionRef(s); + + s.Tell(new GetForwarder(TestActor)); + var f = ExpectMsg(); + forwarder.Watch(f); + s.Tell(new DropForwarder(f)); + ExpectMsg(new Forwarded(new Terminated(f, true, false), f)); + } + + [Fact] + public void FunctionRef_created_by_top_level_actor_must_terminate_when_their_parent_terminates() + { + var s = SuperActor(); + var forwarder = GetFunctionRef(s); + + Watch(forwarder); + s.Tell(PoisonPill.Instance); + ExpectTerminated(forwarder); + } + + private FunctionRef GetFunctionRef(IActorRef s) + { + s.Tell(new GetForwarder(TestActor)); + return ExpectMsg(); + } + + private IActorRef SuperActor() => Sys.ActorOf(Props.Create(), "super"); + + #endregion + + #region non-top level + + [Fact] + public void FunctionRef_created_by_non_top_level_actor_must_forward_messages() + { + var s = SupSuperActor(); + var forwarder = GetFunctionRef(s); + + forwarder.Tell("hello"); + ExpectMsg(new Forwarded("hello", TestActor)); + } + + [Fact] + public void FunctionRef_created_by_non_top_level_actor_must_be_watchable() + { + var s = SupSuperActor(); + var forwarder = GetFunctionRef(s); + + s.Tell(new GetForwarder(TestActor)); + var f = ExpectMsg(); + Watch(f); + s.Tell(new DropForwarder(f)); + ExpectTerminated(f); + } + + [Fact] + public void FunctionRef_created_by_non_top_level_actor_must_be_able_to_watch() + { + var s = SupSuperActor(); + var forwarder = GetFunctionRef(s); + + s.Tell(new GetForwarder(TestActor)); + var f = ExpectMsg(); + forwarder.Watch(f); + s.Tell(new DropForwarder(f)); + ExpectMsg(new Forwarded(new Terminated(f, true, false), f)); + } + + [Fact] + public void FunctionRef_created_by_non_top_level_actor_must_terminate_when_their_parent_terminates() + { + var s = SupSuperActor(); + var forwarder = GetFunctionRef(s); + + Watch(forwarder); + s.Tell(PoisonPill.Instance); + ExpectTerminated(forwarder); + } + + private IActorRef SupSuperActor() => Sys.ActorOf(Props.Create(), "supsuper"); + + #endregion + + [Fact(Skip = "FIXME")] + public void FunctionRef_when_not_registered_must_not_be_found() + { + var provider = ((ExtendedActorSystem)Sys).Provider; + var fref = new FunctionRef(TestActor.Path / "blabla", provider, Sys.EventStream, (x, y) => { }); + EventFilter.Exception().ExpectOne(() => + { + // needs to be something that fails when the deserialized form is not a FunctionRef + // this relies upon serialize-messages during tests + TestActor.Tell(new DropForwarder(fref)); + ExpectNoMsg(TimeSpan.FromSeconds(1)); + }); + } + } +} \ No newline at end of file diff --git a/src/core/Akka/Actor/ActorCell.Children.cs b/src/core/Akka/Actor/ActorCell.Children.cs index 6682b11ecf1..1e1644d5dd2 100644 --- a/src/core/Akka/Actor/ActorCell.Children.cs +++ b/src/core/Akka/Actor/ActorCell.Children.cs @@ -7,6 +7,8 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; using System.Threading; using Akka.Actor.Internal; using Akka.Serialization; @@ -19,6 +21,7 @@ public partial class ActorCell { private volatile IChildrenContainer _childrenContainerDoNotCallMeDirectly = EmptyChildrenContainer.Instance; private long _nextRandomNameDoNotCallMeDirectly = -1; // Interlocked.Increment automatically adds 1 to this value. Allows us to start from 0. + private ImmutableDictionary _functionRefsDoNotCallMeDirectly = ImmutableDictionary.Empty; /// /// The child container collection, used to house information about all child actors. @@ -33,6 +36,45 @@ private IReadOnlyCollection Children get { return ChildrenContainer.Children; } } + private ImmutableDictionary FunctionRefs => Volatile.Read(ref _functionRefsDoNotCallMeDirectly); + internal bool TryGetFunctionRef(string name, out FunctionRef functionRef) => + FunctionRefs.TryGetValue(name, out functionRef); + + internal bool TryGetFunctionRef(string name, int uid, out FunctionRef functionRef) => + FunctionRefs.TryGetValue(name, out functionRef) && (uid == ActorCell.UndefinedUid || uid == functionRef.Path.Uid); + + internal FunctionRef AddFunctionRef(Action tell, string suffix = "") + { + var r = GetRandomActorName("$$"); + var n = string.IsNullOrEmpty(suffix) ? r : r + "-" + suffix; + var childPath = new ChildActorPath(Self.Path, n, NewUid()); + var functionRef = new FunctionRef(childPath, SystemImpl.Provider, SystemImpl.EventStream, tell); + + return ImmutableInterlocked.GetOrAdd(ref _functionRefsDoNotCallMeDirectly, childPath.Name, functionRef); + } + + internal bool RemoveFunctionRef(FunctionRef functionRef) + { + if (functionRef.Path.Parent != Self.Path) throw new InvalidOperationException($"Trying to remove FunctionRef {functionRef.Path} from wrong ActorCell"); + + var name = functionRef.Path.Name; + if (ImmutableInterlocked.TryRemove(ref _functionRefsDoNotCallMeDirectly, name, out var fref)) + { + fref.Stop(); + return true; + } + else return false; + } + + protected void StopFunctionRefs() + { + var refs = Interlocked.Exchange(ref _functionRefsDoNotCallMeDirectly, ImmutableDictionary.Empty); + foreach (var pair in refs) + { + pair.Value.Stop(); + } + } + /// /// Attaches a child to the current . /// @@ -86,11 +128,12 @@ private IActorRef ActorOf(Props props, string name, bool isAsync, bool isSystemS return MakeChild(props, name, isAsync, isSystemService); } - - private string GetRandomActorName() + + private string GetRandomActorName(string prefix = "$") { var id = Interlocked.Increment(ref _nextRandomNameDoNotCallMeDirectly); - return "$" + id.Base64Encode(); + var sb = new StringBuilder(prefix); + return id.Base64Encode(sb).ToString(); } /// @@ -346,18 +389,21 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) if (name.IndexOf('#') < 0) { // optimization for the non-uid case - ChildRestartStats stats; - if (TryGetChildRestartStatsByName(name, out stats)) + if (TryGetChildRestartStatsByName(name, out var stats)) { child = stats.Child; return true; } + else if (TryGetFunctionRef(name, out var functionRef)) + { + child = functionRef; + return true; + } } else { var nameAndUid = SplitNameAndUid(name); - ChildRestartStats stats; - if (TryGetChildRestartStatsByName(nameAndUid.Name, out stats)) + if (TryGetChildRestartStatsByName(nameAndUid.Name, out var stats)) { var uid = nameAndUid.Uid; if (uid == ActorCell.UndefinedUid || uid == stats.Uid) @@ -366,6 +412,11 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) return true; } } + else if (TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef)) + { + child = functionRef; + return true; + } } child = ActorRefs.Nobody; return false; @@ -378,8 +429,7 @@ public bool TryGetSingleChild(string name, out IInternalActorRef child) /// TBD protected SuspendReason RemoveChildAndGetStateChange(IActorRef child) { - var terminating = ChildrenContainer as TerminatingChildrenContainer; - if (terminating != null) + if (ChildrenContainer is TerminatingChildrenContainer terminating) { var newContainer = UpdateChildrenRefs(c => c.Remove(child)); if (newContainer is TerminatingChildrenContainer) return null; diff --git a/src/core/Akka/Actor/ActorCell.FaultHandling.cs b/src/core/Akka/Actor/ActorCell.FaultHandling.cs index 09a5eb17ef4..cd0825d12bb 100644 --- a/src/core/Akka/Actor/ActorCell.FaultHandling.cs +++ b/src/core/Akka/Actor/ActorCell.FaultHandling.cs @@ -298,26 +298,30 @@ private void FinishTerminate() } finally { - try{ Dispatcher.Detach(this); } + try { Dispatcher.Detach(this); } finally { try { Parent.SendSystemMessage(new DeathWatchNotification(_self, existenceConfirmed: true, addressTerminated: false)); } finally { - try { TellWatchersWeDied(); } + try { StopFunctionRefs(); } finally { - try { UnwatchWatchedActors(a); } // stay here as we expect an emergency stop from HandleInvokeFailure + try { TellWatchersWeDied(); } finally { - if (System.Settings.DebugLifecycle) - Publish(new Debug(_self.Path.ToString(), ActorType, "Stopped")); + try { UnwatchWatchedActors(a); } // stay here as we expect an emergency stop from HandleInvokeFailure + finally + { + if (System.Settings.DebugLifecycle) + Publish(new Debug(_self.Path.ToString(), ActorType, "Stopped")); - ClearActor(a); - ClearActorCell(); + ClearActor(a); + ClearActorCell(); - _actor = null; + _actor = null; + } } } } diff --git a/src/core/Akka/Actor/ActorRef.cs b/src/core/Akka/Actor/ActorRef.cs index 824c8ff416d..e288d03aa64 100644 --- a/src/core/Akka/Actor/ActorRef.cs +++ b/src/core/Akka/Actor/ActorRef.cs @@ -9,6 +9,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -391,7 +392,7 @@ public interface IInternalActorRef : IActorRef, IActorRefScope IActorRefProvider Provider { get; } /// - /// Obsolete. Use or Receive<> + /// Obsolete. Use or Receive<> /// [Obsolete("Use Context.Watch and Receive [1.1.0]")] bool IsTerminated { get; } @@ -842,5 +843,203 @@ IEnumerator IEnumerable.GetEnumerator() } } } -} + /// + /// INTERNAL API + /// + /// This kind of ActorRef passes all received messages to the given function for + /// performing a non-blocking side-effect. The intended use is to transform the + /// message before sending to the real target actor. Such references can be created + /// by calling and must be deregistered when no longer + /// needed by calling . FunctionRefs do not count + /// towards the live children of an actor, they do not receive the Terminate command + /// and do not prevent the parent from terminating. FunctionRef is properly + /// registered for remote lookup and ActorSelection. + /// + /// When using the feature you must ensure that upon reception of the + /// Terminated message the watched actorRef is ed. + /// + internal sealed class FunctionRef : MinimalActorRef + { + private readonly EventStream _eventStream; + private readonly Action _tell; + + private ImmutableHashSet _watching = ImmutableHashSet.Empty; + private ImmutableHashSet _watchedBy = ImmutableHashSet.Empty; + + public FunctionRef(ActorPath path, IActorRefProvider provider, EventStream eventStream, Action tell) + { + _eventStream = eventStream; + _tell = tell; + Path = path; + Provider = provider; + } + + public override ActorPath Path { get; } + public override IActorRefProvider Provider { get; } + public override bool IsTerminated => Volatile.Read(ref _watchedBy) == null; + + /// + /// Have this FunctionRef watch the given Actor. This method must not be + /// called concurrently from different threads, it should only be called by + /// its parent Actor. + /// + /// Upon receiving the Terminated message, must be called from a + /// safe context (i.e. normally from the parent Actor). + /// + public void Watch(IActorRef actorRef) + { + _watching = _watching.Add(actorRef); + var internalRef = (IInternalActorRef)actorRef; + internalRef.SendSystemMessage(new Watch(internalRef, this)); + } + + /// + /// Have this FunctionRef unwatch the given Actor. This method must not be + /// called concurrently from different threads, it should only be called by + /// its parent Actor. + /// + public void Unwatch(IActorRef actorRef) + { + _watching = _watching.Remove(actorRef); + var internalRef = (IInternalActorRef)actorRef; + internalRef.SendSystemMessage(new Unwatch(internalRef, this)); + + } + + /// + /// Query whether this FunctionRef is currently watching the given Actor. This + /// method must not be called concurrently from different threads, it should + /// only be called by its parent Actor. + /// + public bool IsWatching(IActorRef actorRef) => _watching.Contains(actorRef); + + protected override void TellInternal(object message, IActorRef sender) => _tell(sender, message); + + public override void SendSystemMessage(ISystemMessage message) + { + switch (message) + { + case Watch watch: + AddWatcher(watch.Watchee, watch.Watcher); + break; + case Unwatch unwatch: + RemoveWatcher(unwatch.Watchee, unwatch.Watcher); + break; + case DeathWatchNotification deathWatch: + this.Tell(new Terminated(deathWatch.Actor, existenceConfirmed: true, addressTerminated: false), deathWatch.Actor); + break; + } + } + + private void SendTerminated() + { + var watchedBy = Interlocked.Exchange(ref _watchedBy, null); + if (watchedBy != null) + { + if (!watchedBy.IsEmpty) + { + foreach (var watcher in watchedBy) + SendTerminated(watcher); + } + + if (!_watching.IsEmpty) + { + foreach (var watched in _watching) + UnwatchWatched(watched); + + _watching = ImmutableHashSet.Empty; + } + } + } + + private void SendTerminated(IActorRef watcher) + { + if (watcher is IInternalActorRef scope) + scope.SendSystemMessage(new DeathWatchNotification(this, existenceConfirmed: true, addressTerminated: false)); + } + + private void UnwatchWatched(IActorRef watched) + { + if (watched is IInternalActorRef internalActorRef) + internalActorRef.SendSystemMessage(new Unwatch(internalActorRef, this)); + } + + public override void Stop() => SendTerminated(); + + private void AddWatcher(IInternalActorRef watchee, IInternalActorRef watcher) + { + while (true) + { + var watchedBy = Volatile.Read(ref _watchedBy); + if (watchedBy == null) + SendTerminated(watcher); + else + { + var watcheeSelf = Equals(watchee, this); + var watcherSelf = Equals(watcher, this); + + if (watcheeSelf && !watcherSelf) + { + if (!watchedBy.Contains(watcher) && !ReferenceEquals(watchedBy, Interlocked.CompareExchange(ref _watchedBy, watchedBy.Add(watcher), watchedBy))) + { + continue; + } + } + else if (!watcheeSelf && watcherSelf) + { + Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"Externally triggered watch from {watcher} to {watchee} is illegal on FunctionRef")); + } + else + { + Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"BUG: illegal Watch({watchee},{watcher}) for {this}")); + } + } + + break; + } + } + + private void RemoveWatcher(IInternalActorRef watchee, IInternalActorRef watcher) + { + while (true) + { + var watchedBy = Volatile.Read(ref _watchedBy); + if (watchedBy == null) + SendTerminated(watcher); + else + { + var watcheeSelf = Equals(watchee, this); + var watcherSelf = Equals(watcher, this); + + if (watcheeSelf && !watcherSelf) + { + if (!watchedBy.Contains(watcher) && !ReferenceEquals(watchedBy, Interlocked.CompareExchange(ref _watchedBy, watchedBy.Remove(watcher), watchedBy))) + { + continue; + } + } + else if (!watcheeSelf && watcherSelf) + { + Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"Externally triggered watch from {watcher} to {watchee} is illegal on FunctionRef")); + } + else + { + Publish(new Warning(Path.ToString(), typeof(FunctionRef), $"BUG: illegal Watch({watchee},{watcher}) for {this}")); + } + } + + break; + } + } + + private void Publish(LogEvent e) + { + try + { + _eventStream.Publish(e); + } + catch (Exception) { } + } + } +} \ No newline at end of file diff --git a/src/core/Akka/Actor/IAutoReceivedMessage.cs b/src/core/Akka/Actor/IAutoReceivedMessage.cs index 4759f0f303e..62a1a703c53 100644 --- a/src/core/Akka/Actor/IAutoReceivedMessage.cs +++ b/src/core/Akka/Actor/IAutoReceivedMessage.cs @@ -5,6 +5,7 @@ // //----------------------------------------------------------------------- +using System; using Akka.Event; namespace Akka.Actor @@ -21,7 +22,7 @@ public interface IAutoReceivedMessage /// Terminated message can't be forwarded to another actor, since that actor might not be watching the subject. /// Instead, if you need to forward Terminated to another actor you should send the information in your own message. /// - public sealed class Terminated : IAutoReceivedMessage, IPossiblyHarmful, IDeadLetterSuppression, INoSerializationVerificationNeeded + public sealed class Terminated : IAutoReceivedMessage, IPossiblyHarmful, IDeadLetterSuppression, INoSerializationVerificationNeeded, IEquatable { /// /// Initializes a new instance of the class. @@ -61,6 +62,26 @@ public override string ToString() { return $": {ActorRef} - ExistenceConfirmed={ExistenceConfirmed}"; } + + public bool Equals(Terminated other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Equals(ActorRef, other.ActorRef) && AddressTerminated == other.AddressTerminated && ExistenceConfirmed == other.ExistenceConfirmed; + } + + public override bool Equals(object obj) => obj is Terminated terminated && Equals(terminated); + + public override int GetHashCode() + { + unchecked + { + var hashCode = (ActorRef != null ? ActorRef.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ AddressTerminated.GetHashCode(); + hashCode = (hashCode * 397) ^ ExistenceConfirmed.GetHashCode(); + return hashCode; + } + } } /// diff --git a/src/core/Akka/Actor/RepointableActorRef.cs b/src/core/Akka/Actor/RepointableActorRef.cs index 8c5f44bd169..606cf83adb5 100644 --- a/src/core/Akka/Actor/RepointableActorRef.cs +++ b/src/core/Akka/Actor/RepointableActorRef.cs @@ -293,8 +293,7 @@ public override IActorRef GetChild(IEnumerable name) return ActorRefs.Nobody; default: var nameAndUid = ActorCell.SplitNameAndUid(next); - IChildStats stats; - if (Lookup.TryGetChildStatsByName(nameAndUid.Name, out stats)) + if (Lookup.TryGetChildStatsByName(nameAndUid.Name, out var stats)) { var crs = stats as ChildRestartStats; var uid = nameAndUid.Uid; @@ -306,6 +305,10 @@ public override IActorRef GetChild(IEnumerable name) return crs.Child; } } + else if (Lookup is ActorCell cell && cell.TryGetFunctionRef(nameAndUid.Name, nameAndUid.Uid, out var functionRef)) + { + return functionRef; + } return ActorRefs.Nobody; } } @@ -342,7 +345,7 @@ public class UnstartedCell : ICell private readonly IInternalActorRef _supervisor; private readonly object _lock = new object(); - /* Both queues must be accessed via lock */ + /* Both queues must be accessed via lock */ private readonly LinkedList _messageQueue = new LinkedList(); private LatestFirstSystemMessageList _sysMsgQueue = SystemMessageList.LNil; diff --git a/src/core/Akka/Util/Base64Encoding.cs b/src/core/Akka/Util/Base64Encoding.cs index 13d7a6d465f..493d89e0ffb 100644 --- a/src/core/Akka/Util/Base64Encoding.cs +++ b/src/core/Akka/Util/Base64Encoding.cs @@ -24,17 +24,23 @@ public static class Base64Encoding /// /// TBD /// TBD - public static string Base64Encode(this long value) + public static string Base64Encode(this long value) => Base64Encode(value, new StringBuilder()).ToString(); + + /// + /// TBD + /// + /// TBD + /// TBD + public static StringBuilder Base64Encode(this long value, StringBuilder sb) { - var sb = new StringBuilder(); var next = value; do { var index = (int)(next & 63); sb.Append(Base64Chars[index]); next = next >> 6; - } while(next != 0); - return sb.ToString(); + } while (next != 0); + return sb; } /// From 691ced4db13711f4ac438f8fea98dcfaea64f152 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Fri, 6 Jul 2018 16:38:24 +0200 Subject: [PATCH 10/14] reintroduced stream refs --- build.fsx | 3 +- docs/articles/streams/streamrefs.md | 88 + .../Streams/StreamRefsDocTests.cs | 140 ++ docs/images/sink-ref-animation.gif | Bin 0 -> 96908 bytes docs/images/source-ref-animation.gif | Bin 0 -> 96884 bytes src/Akka.sln | 3 +- .../Akka.Streams.Tests.csproj | 1 + .../Akka.Streams.Tests/Dsl/StreamRefsSpec.cs | 407 ++++ src/core/Akka.Streams/ActorMaterializer.cs | 36 +- src/core/Akka.Streams/Akka.Streams.csproj | 1 + src/core/Akka.Streams/Attributes.cs | 27 + src/core/Akka.Streams/Dsl/StreamRefs.cs | 733 +++++++ .../Akka.Streams/Implementation/Buffers.cs | 2 + .../Proto/StreamRefMessages.g.cs | 1709 +++++++++++++++++ .../Serialization/StreamRefSerializer.cs | 237 +++ src/core/Akka.Streams/StreamRefs.cs | 114 ++ src/core/Akka.Streams/reference.conf | 40 + src/protobuf/StreamRefMessages.proto | 58 + 18 files changed, 3585 insertions(+), 14 deletions(-) create mode 100644 docs/articles/streams/streamrefs.md create mode 100644 docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs create mode 100644 docs/images/sink-ref-animation.gif create mode 100644 docs/images/source-ref-animation.gif create mode 100644 src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs create mode 100644 src/core/Akka.Streams/Dsl/StreamRefs.cs create mode 100644 src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs create mode 100644 src/core/Akka.Streams/Serialization/StreamRefSerializer.cs create mode 100644 src/core/Akka.Streams/StreamRefs.cs create mode 100644 src/protobuf/StreamRefMessages.proto diff --git a/build.fsx b/build.fsx index 44c456b3cc2..e408fa9bf14 100644 --- a/build.fsx +++ b/build.fsx @@ -452,7 +452,8 @@ Target "Protobuf" <| fun _ -> ("DistributedPubSubMessages.proto", "/src/contrib/cluster/Akka.Cluster.Tools/PublishSubscribe/Serialization/Proto/"); ("ClusterShardingMessages.proto", "/src/contrib/cluster/Akka.Cluster.Sharding/Serialization/Proto/"); ("TestConductorProtocol.proto", "/src/core/Akka.Remote.TestKit/Proto/"); - ("Persistence.proto", "/src/core/Akka.Persistence/Serialization/Proto/") ] + ("Persistence.proto", "/src/core/Akka.Persistence/Serialization/Proto/"); + ("StreamRefMessages.proto", "/src/core/Akka.Streams/Serialization/Proto/") ] printfn "Using proto.exe: %s" protocPath diff --git a/docs/articles/streams/streamrefs.md b/docs/articles/streams/streamrefs.md new file mode 100644 index 00000000000..c63a06bc44a --- /dev/null +++ b/docs/articles/streams/streamrefs.md @@ -0,0 +1,88 @@ +--- +uid: stream-ref +title: StreamRefs - Reactive Streams over the network +--- + +> **Warning** +This module is currently marked as may change in the sense of being the subject of active research. This means that API or semantics can change without warning or deprecation period and it is not recommended to use this module in production just yet—you have been warned. + +Stream references, or “stream refs” for short, allow running Akka Streams across multiple nodes within an Akka Remote boundaries. + +Unlike heavier “streaming data processing” frameworks, Akka Streams are not “deployed” nor automatically distributed. Akka stream refs are, as the name implies, references to existing parts of a stream, and can be used to create a distributed processing framework or introduce such capabilities in specific parts of your application. + +Stream refs are trivial to make use of in existing clustered Akka applications, and require no additional configuration or setup. They automatically maintain flow-control / back-pressure over the network, and employ Akka’s failure detection mechanisms to fail-fast (“let it crash!”) in the case of failures of remote nodes. They can be seen as an implementation of the Work Pulling Pattern, which one would otherwise implement manually. + +> **Note** +A useful way to think about stream refs is: “like an `IActorRef`, but for Akka Streams’s `Source` and `Sink`”. +Stream refs refer to an already existing, possibly remote, `Sink` or `Source`. This is not to be mistaken with deploying streams remotely, which this feature is not intended for. + +## Stream References + +The prime use case for stream refs is to replace raw actor or HTTP messaging between systems where a long running stream of data is expected between two entities. Often times, they can be used to effectively achieve point to point streaming without the need of setting up additional message brokers or similar secondary clusters. + +Stream refs are well suited for any system in which you need to send messages between nodes and need to do so in a flow-controlled fashion. Typical examples include sending work requests to worker nodes, as fast as possible, but not faster than the worker node can process them, or sending data elements which the downstream may be slow at processing. It is recommended to mix and introduce stream refs in Actor messaging based systems, where the actor messaging is used to orchestrate and prepare such message flows, and later the stream refs are used to do the flow-controlled message transfer. + +Stream refs are not persistent, however it is simple to build a recover-able stream by introducing such protocol on the actor messaging layer. Stream refs are absolutely expected to be sent over Akka remoting to other nodes within a cluster, and as such, complement and do not compete with plain Actor messaging. Actors would usually be used to establish the stream, by means of some initial message saying “I want to offer you many log elements (the stream ref)”, or alternatively in the opposite way “If you need to send me much data, here is the stream ref you can use to do so”. + +Since the two sides (“local” and “remote”) of each reference may be confusing to simply refer to as “remote” and “local” – since either side can be seen as “local” or “remote” depending how we look at it – we propose to use the terminology “origin” and “target”, which is defined by where the stream ref was created. For SourceRefs, the “origin” is the side which has the data that it is going to stream out. For SinkRefs the “origin” side is the actor system that is ready to receive the data and has allocated the ref. Those two may be seen as duals of each other, however to explain patterns about sharing references, we found this wording to be rather useful. + +### Source Refs - offering streaming data to a remote system + +A `SourceRef` can be offered to a remote actor system in order for it to consume some source of data that we have prepared locally. + +In order to share a `Source` with a remote endpoint you need to materialize it by running it into the `StreamRefs.SourceRef`. That sink materializes the `ISourceRef` that you can then send to other nodes. Please note that it materializes into a Task so you will have to use the continuation (either `PipeTo` or async/await pattern). + +[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=data-source-actor)] + +The origin actor which creates and owns the `Source` could also perform some validation or additional setup when preparing the source. Once it has handed out the `ISourceRef` the remote side can run it like this: + +[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=source-ref-materialization)] + +The process of preparing and running a `ISourceRef` powered distributed stream is shown by the animation below: + +![source ref](/images/source-ref-animation.gif) + +> **Warning** +A `ISourceRef` is by design “single-shot”. i.e. it may only be materialized once. This is in order to not complicate the mental model what materializing such value would mean. +While stream refs are designed to be single shot, you may use them to mimic multicast scenarios, simply by starting a `Broadcast` stage once, and attaching multiple new streams to it, for each emitting a new stream ref. This way each output of the broadcast is by itself an unique single-shot reference, however they can all be powered using a single `Source` – located before the `Broadcast` stage. + +### Sink Refs - offering to receive streaming data from a remote system + +They can be used to offer the other side the capability to send to the origin side data in a streaming, flow-controlled fashion. The origin here allocates a Sink, which could be as simple as a `Sink.ForEach` or as advanced as a complex sink which streams the incoming data into various other systems (e.g. any of the Alpakka provided Sinks). + +> **Note** +To form a good mental model of `SinkRef`s, you can think of them as being similar to “passive mode” in FTP. + +[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=data-sink-actor)] + +Using the offered `ISinkRef<>` to send data to the origin of the `Sink` is also simple, as we can treat the `ISinkRef<>` just as any other sink and directly runWith or run with it. + +[!code-csharp[StreamRefsDocTests.cs](../../examples/DocsExamples/Streams/StreamRefsDocTests.cs?name=sink-ref-materialization)] + +The process of preparing and running a `ISinkRef<>` powered distributed stream is shown by the animation below: + +![sink ref](/images/sink-ref-animation.gif) + +> **Warning** +A `ISinkRef<>` is *by design* “single-shot”. i.e. it may only be materialized once. This is in order to not complicate the mental model what materializing such value would mean. If you have an use case for building a fan-in operation accepting writes from multiple remote nodes, you can build your `Sink` and prepend it with a `Merge` stage, each time materializing a new `ISinkRef<>` targeting that `Merge`. This has the added benefit of giving you full control how to merge these streams (i.e. by using “merge preferred” or any other variation of the fan-in stages). + +## Configuration + +### Stream reference subscription timeouts + +All stream references have a subscription timeout, which is intended to prevent resource leaks in situations in which a remote node would requests the allocation of many streams yet never actually run them. In order to prevent this, each stream reference has a default timeout (of 30 seconds), after which the origin will abort the stream offer if the target has not materialized the stream ref in time. After the timeout has triggered, materialization of the target side will fail pointing out that the origin is missing. + +Since these timeouts are often very different based on the kind of stream offered, and there can be many different kinds of them in the same application, it is possible to not only configure this setting globally (`akka.stream.materializer.stream-ref.subscription-timeout`), but also via attributes: + +```csharp +Source.Repeat("hello") + .RunWith(StreamRefs.SourceRef() + .AddAttributes(StreamRefAttributes + .SubscriptionTimeout(TimeSpan.FromSeconds(5))) + , materializer); + +StreamRefs.SinkRef() + .AddAttributes(StreamRefAttributes + .SubscriptionTimeout(TimeSpan.FromSeconds(5))) + .RunWith(Sink.Ignore(), materializer); +``` \ No newline at end of file diff --git a/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs b/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs new file mode 100644 index 00000000000..1566638ae85 --- /dev/null +++ b/docs/examples/DocsExamples/Streams/StreamRefsDocTests.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Akka; +using Akka.Streams; +using Akka.Streams.Dsl; +using Akka.TestKit.Xunit2; +using Xunit; +using Xunit.Abstractions; +using Akka.Actor; +using Akka.IO; +using Akka.Util; +using System.Linq; + +namespace DocsExamples.Streams +{ + public class StreamRefsDocTests : TestKit + { + #region data-source-actor + public sealed class RequestLogs + { + public int StreamId { get; } + + public RequestLogs(int streamId) + { + StreamId = streamId; + } + } + + public sealed class LogsOffer + { + public int StreamId { get; } + public ISourceRef SourceRef { get; } + + public LogsOffer(int streamId, ISourceRef sourceRef) + { + StreamId = streamId; + SourceRef = sourceRef; + } + } + + public class DataSource : ReceiveActor + { + public DataSource() + { + Receive(request => + { + // create a source + StreamLogs(request.StreamId) + // materialize it using stream refs + .RunWith(StreamRefs.SourceRef(), Context.System.Materializer()) + // and send to sender + .PipeTo(Sender, success: sourceRef => new LogsOffer(request.StreamId, sourceRef)); + }); + } + + private Source StreamLogs(int streamId) => + Source.From(Enumerable.Range(1, 100)).Select(i => i.ToString()); + } + #endregion + + #region data-sink-actor + public sealed class PrepareUpload + { + public string Id { get; } + public PrepareUpload(string id) + { + Id = id; + } + } + + public sealed class MeasurementsSinkReady + { + public string Id { get; } + public ISinkRef SinkRef { get; } + public MeasurementsSinkReady(string id, ISinkRef sinkRef) + { + Id = id; + SinkRef = sinkRef; + } + } + + class DataReceiver : ReceiveActor + { + public DataReceiver() + { + Receive(prepare => + { + // obtain a source you want to offer + var sink = LogsSinksFor(prepare.Id); + + // materialize sink ref (remote is source data for us) + StreamRefs.SinkRef() + .To(sink) + .Run(Context.System.Materializer()) + .PipeTo(Sender, success: sinkRef => new MeasurementsSinkReady(prepare.Id, sinkRef)); + }); + } + + private Sink LogsSinksFor(string id) => + Sink.ForEach(Console.WriteLine); + } + #endregion + + private ActorMaterializer Materializer { get; } + + public StreamRefsDocTests(ITestOutputHelper output) + : base("", output) + { + Materializer = Sys.Materializer(); + } + + [Fact] + public async Task SourceRef_must_propagate_source_from_another_system() + { + #region source-ref-materialization + var sourceActor = Sys.ActorOf(Props.Create(), "dataSource"); + + var offer = await sourceActor.Ask(new RequestLogs(1337)); + await offer.SourceRef.Source.RunForeach(Console.WriteLine, Materializer); + #endregion + } + + [Fact] + public async Task SinkRef_must_receive_messages_from_another_system() + { + #region sink-ref-materialization + var receiver = Sys.ActorOf(Props.Create(), "receiver"); + + var ready = await receiver.Ask(new PrepareUpload("id"), timeout: TimeSpan.FromSeconds(30)); + + // stream local metrics to Sink's origin: + Source.From(Enumerable.Range(1, 100)) + .Select(i => i.ToString()) + .RunWith(ready.SinkRef.Sink, Materializer); + #endregion + } + } +} \ No newline at end of file diff --git a/docs/images/sink-ref-animation.gif b/docs/images/sink-ref-animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..52e26d2103c04b9350176956f35bed0aede9aa41 GIT binary patch literal 96908 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/images/source-ref-animation.gif b/docs/images/source-ref-animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..76fd2c936dc075e821534e11ccf666d3be529b06 GIT binary patch literal 96884 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 z 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 + diff --git a/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs b/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs new file mode 100644 index 00000000000..ace576233f8 --- /dev/null +++ b/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs @@ -0,0 +1,407 @@ +#region copyright +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- +#endregion + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Actor.Internal; +using Akka.Configuration; +using Akka.IO; +using Akka.Streams.Dsl; +using Akka.Streams.Implementation; +using Akka.Streams.TestKit; +using Akka.TestKit; +using Xunit; +using Xunit.Abstractions; +using FluentAssertions; + +namespace Akka.Streams.Tests +{ + internal sealed class DataSourceActor : ActorBase + { + public static Props Props(IActorRef probe) => + Akka.Actor.Props.Create(() => new DataSourceActor(probe));//.WithDispatcher("akka.test.stream-dispatcher"); + + private readonly IActorRef _probe; + private readonly ActorMaterializer _materializer; + + public DataSourceActor(IActorRef probe) + { + _probe = probe; + _materializer = Context.System.Materializer(); + } + + protected override void PostStop() + { + base.PostStop(); + _materializer.Dispose(); + } + + protected override bool Receive(object message) + { + switch (message) + { + case "give": + { + /* + * Here we're able to send a source to a remote recipient + * For them it's a Source; for us it is a Sink we run data "into" + */ + var source = Source.From(new[] { "hello", "world" }); + var aref = source.RunWith(StreamRefs.SourceRef(), _materializer); + aref.PipeTo(Sender); + return true; + } + case "give-infinite": + { + var source = Source.From(Enumerable.Range(1, int.MaxValue).Select(i => "ping-" + i)); + var t = source.ToMaterialized(StreamRefs.SourceRef(), Keep.Right).Run(_materializer); + t.PipeTo(Sender); + return true; + } + case "give-fail": + { + var r = Source.Failed(new Exception("Boom!")) + .RunWith(StreamRefs.SourceRef(), _materializer); + r.PipeTo(Sender); + return true; + } + case "give-complete-asap": + { + var r = Source.Empty().RunWith(StreamRefs.SourceRef(), _materializer); + r.PipeTo(Sender); + return true; + } + case "give-subscribe-timeout": + { + var r = Source.Repeat("is anyone there?") + .ToMaterialized(StreamRefs.SourceRef(), Keep.Right) + .WithAttributes(StreamRefAttributes.CreateSubscriptionTimeout(TimeSpan.FromMilliseconds(500))) + .Run(_materializer); + r.PipeTo(Sender); + return true; + } + case "receive": + { + /* + * We write out code, knowing that the other side will stream the data into it. + * For them it's a Sink; for us it's a Source. + */ + var sink = StreamRefs.SinkRef().To(Sink.ActorRef(_probe, "")) + .Run(_materializer); + sink.PipeTo(Sender); + return true; + } + case "receive-subscribe-timeout": + { + var sink = StreamRefs.SinkRef() + .WithAttributes(StreamRefAttributes.CreateSubscriptionTimeout(TimeSpan.FromMilliseconds(500))) + .To(Sink.ActorRef(_probe, "")) + .Run(_materializer); + sink.PipeTo(Sender); + return true; + } + case "receive-32": + { + // var t = StreamRefs.SinkRef() + // .ToMaterialized(TestSink.SinkProbe(Context.System), Keep.Both) + // .Run(_materializer); + // + // var sink = t.Item1; + // var driver = t.Item2; + // Task.Run(() => + // { + // driver.EnsureSubscription(); + // driver.Request(2); + // driver.ExpectNext(); + // driver.ExpectNext(); + // driver.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + // driver.Request(30); + // driver.ExpectNextN(30); + // + // return ""; + // }).PipeTo(_probe); + + return true; + } + default: return false; + } + } + } + + internal sealed class SourceMsg + { + public ISourceRef DataSource { get; } + + public SourceMsg(ISourceRef dataSource) + { + DataSource = dataSource; + } + } + + internal sealed class BulkSourceMsg + { + public ISourceRef DataSource { get; } + + public BulkSourceMsg(ISourceRef dataSource) + { + DataSource = dataSource; + } + } + + internal sealed class SinkMsg + { + public ISinkRef DataSink { get; } + + public SinkMsg(ISinkRef dataSink) + { + DataSink = dataSink; + } + } + + internal sealed class BulkSinkMsg + { + public ISinkRef DataSink { get; } + + public BulkSinkMsg(ISinkRef dataSink) + { + DataSink = dataSink; + } + } + + public class StreamRefsSpec : AkkaSpec + { + public static Config Config() + { + var address = TestUtils.TemporaryServerAddress(); + return ConfigurationFactory.ParseString($@" + akka {{ + loglevel = INFO + actor {{ + provider = remote + serialize-messages = off + }} + remote.dot-netty.tcp {{ + port = {address.Port} + hostname = ""{address.Address}"" + }} + }}").WithFallback(ConfigurationFactory.Load()); + } + + public StreamRefsSpec(ITestOutputHelper output) : this(Config(), output: output) + { + } + + protected StreamRefsSpec(Config config, ITestOutputHelper output = null) : base(config, output) + { + Materializer = Sys.Materializer(); + RemoteSystem = ActorSystem.Create("remote-system", Config()); + InitializeLogger(RemoteSystem); + _probe = CreateTestProbe(); + + var it = RemoteSystem.ActorOf(DataSourceActor.Props(_probe.Ref), "remoteActor"); + var remoteAddress = ((ActorSystemImpl)RemoteSystem).Provider.DefaultAddress; + Sys.ActorSelection(it.Path.ToStringWithAddress(remoteAddress)).Tell(new Identify("hi")); + + _remoteActor = ExpectMsg().Subject; + } + + protected readonly ActorSystem RemoteSystem; + protected readonly ActorMaterializer Materializer; + private readonly TestProbe _probe; + private readonly IActorRef _remoteActor; + + protected override void BeforeTermination() + { + base.BeforeTermination(); + RemoteSystem.Dispose(); + Materializer.Dispose(); + } + + [Fact] + public void SourceRef_must_send_messages_via_remoting() + { + _remoteActor.Tell("give"); + var sourceRef = ExpectMsg>(); + + sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); + + _probe.ExpectMsg("hello"); + _probe.ExpectMsg("world"); + _probe.ExpectMsg(""); + } + + [Fact] + public void SourceRef_must_fail_when_remote_source_failed() + { + _remoteActor.Tell("give-fail"); + var sourceRef = ExpectMsg>(); + + sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); + + var f = _probe.ExpectMsg(); + f.Cause.Message.Should().Contain("Remote stream ("); + f.Cause.Message.Should().Contain("Boom!"); + } + + [Fact] + public void SourceRef_must_complete_properly_when_remote_source_is_empty() + { + // this is a special case since it makes sure that the remote stage is still there when we connect to it + _remoteActor.Tell("give-complete-asap"); + var sourceRef = ExpectMsg>(); + + sourceRef.Source.RunWith(Sink.ActorRef(_probe.Ref, ""), Materializer); + + _probe.ExpectMsg(""); + } + + [Fact] + public void SourceRef_must_respect_backpressure_from_implied_by_target_Sink() + { + _remoteActor.Tell("give-infinite"); + var sourceRef = ExpectMsg>(); + + var probe = sourceRef.Source.RunWith(this.SinkProbe(), Materializer); + + probe.EnsureSubscription(); + probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + + probe.Request(1); + probe.ExpectNext("ping-1"); + probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + + probe.Request(20); + probe.ExpectNextN(Enumerable.Range(1, 20).Select(i => "ping-" + (i + 1))); + probe.Cancel(); + + // since no demand anyway + probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + + // should not cause more pulling, since we issued a cancel already + probe.Request(10); + probe.ExpectNoMsg(TimeSpan.FromMilliseconds(100)); + } + + [Fact] + public void SourceRef_must_receive_timeout_if_subscribing_too_late_to_the_source_ref() + { + _remoteActor.Tell("give-subscribe-timeout"); + var sourceRef = ExpectMsg>(); + + + // not materializing it, awaiting the timeout... + Thread.Sleep(800); + + var probe = sourceRef.Source.RunWith(this.SinkProbe(), Materializer); + + // the local "remote sink" should cancel, since it should notice the origin target actor is dead + probe.EnsureSubscription(); + var ex = probe.ExpectError(); + ex.Message.Should().Contain("has terminated! Tearing down this side of the stream as well."); + } + + [Fact] + public void SinkRef_must_receive_elements_via_remoting() + { + _remoteActor.Tell("receive"); + var remoteSink = ExpectMsg>(); + + Source.From(new[] { "hello", "world" }) + .To(remoteSink.Sink) + .Run(Materializer); + + _probe.ExpectMsg("hello"); + _probe.ExpectMsg("world"); + _probe.ExpectMsg(""); + } + + [Fact] + public void SinkRef_must_fail_origin_if_remote_Sink_gets_a_failure() + { + _remoteActor.Tell("receive"); + var remoteSink = ExpectMsg>(); + + Source.Failed(new Exception("Boom!")) + .To(remoteSink.Sink) + .Run(Materializer); + + var failure = _probe.ExpectMsg(); + failure.Cause.Message.Should().Contain("Remote stream ("); + failure.Cause.Message.Should().Contain("Boom!"); + } + + [Fact] + public void SinkRef_must_receive_hundreds_of_elements_via_remoting() + { + _remoteActor.Tell("receive"); + var remoteSink = ExpectMsg>(); + + var msgs = Enumerable.Range(1, 100).Select(i => "payload-" + i).ToArray(); + + Source.From(msgs).RunWith(remoteSink.Sink, Materializer); + + foreach (var msg in msgs) + { + _probe.ExpectMsg(msg); + } + + _probe.ExpectMsg(""); + } + + [Fact] + public void SinkRef_must_receive_timeout_if_subscribing_too_late_to_the_sink_ref() + { + _remoteActor.Tell("receive-subscribe-timeout"); + var remoteSink = ExpectMsg>(); + + // not materializing it, awaiting the timeout... + Thread.Sleep(800); + + var probe = this.SourceProbe().To(remoteSink.Sink).Run(Materializer); + + var failure = _probe.ExpectMsg(); + failure.Cause.Message.Should().Contain("Remote side did not subscribe (materialize) handed out Sink reference"); + + // the local "remote sink" should cancel, since it should notice the origin target actor is dead + probe.ExpectCancellation(); + } + + [Fact(Skip = "FIXME: how to pass test assertions to remote system?")] + public void SinkRef_must_respect_backpressure_implied_by_origin_Sink() + { + _remoteActor.Tell("receive-32"); + var sinkRef = ExpectMsg>(); + + Source.Repeat("hello").RunWith(sinkRef.Sink, Materializer); + + // if we get this message, it means no checks in the request/expect semantics were broken, good! + _probe.ExpectMsg(""); + } + + [Fact] + public void SinkRef_must_not_allow_materializing_multiple_times() + { + _remoteActor.Tell("receive-subscribe-timeout"); + var sinkRef = ExpectMsg>(); + + var p1 = this.SourceProbe().To(sinkRef.Sink).Run(Materializer); + var p2 = this.SourceProbe().To(sinkRef.Sink).Run(Materializer); + + p1.EnsureSubscription(); + var req = p1.ExpectRequest(); + + // will be cancelled immediately, since it's 2nd: + p2.EnsureSubscription(); + p2.ExpectCancellation(); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/ActorMaterializer.cs b/src/core/Akka.Streams/ActorMaterializer.cs index 16f1b5d026c..e94d70c95b3 100644 --- a/src/core/Akka.Streams/ActorMaterializer.cs +++ b/src/core/Akka.Streams/ActorMaterializer.cs @@ -318,7 +318,8 @@ private static ActorMaterializerSettings Create(Config config) isFuzzingMode: config.GetBoolean("debug.fuzzing-mode"), isAutoFusing: config.GetBoolean("auto-fusing", true), maxFixedBufferSize: config.GetInt("max-fixed-buffer-size", 1000000000), - syncProcessingLimit: config.GetInt("sync-processing-limit", 1000)); + syncProcessingLimit: config.GetInt("sync-processing-limit", 1000), + streamRefSettings: StreamRefSettings.Create(config.GetConfig("stream-ref") ?? Config.Empty)); } private const int DefaultlMaxFixedbufferSize = 1000; @@ -367,6 +368,8 @@ private static ActorMaterializerSettings Create(Config config) /// public readonly int SyncProcessingLimit; + public readonly StreamRefSettings StreamRefSettings; + /// /// TBD /// @@ -381,7 +384,7 @@ private static ActorMaterializerSettings Create(Config config) /// TBD /// TBD /// TBD - public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Decider supervisionDecider, StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = DefaultlMaxFixedbufferSize) + public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Decider supervisionDecider, StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, StreamRefSettings streamRefSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = DefaultlMaxFixedbufferSize) { InitialInputBufferSize = initialInputBufferSize; MaxInputBufferSize = maxInputBufferSize; @@ -394,7 +397,7 @@ public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferS IsAutoFusing = isAutoFusing; MaxFixedBufferSize = maxFixedBufferSize; SyncProcessingLimit = syncProcessingLimit; - + StreamRefSettings = streamRefSettings; } /// @@ -405,7 +408,7 @@ public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferS /// TBD public ActorMaterializerSettings WithInputBuffer(int initialSize, int maxSize) { - return new ActorMaterializerSettings(initialSize, maxSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(initialSize, maxSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -415,7 +418,7 @@ public ActorMaterializerSettings WithInputBuffer(int initialSize, int maxSize) /// TBD public ActorMaterializerSettings WithDispatcher(string dispatcher) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -425,7 +428,7 @@ public ActorMaterializerSettings WithDispatcher(string dispatcher) /// TBD public ActorMaterializerSettings WithSupervisionStrategy(Decider decider) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, decider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, decider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -435,7 +438,7 @@ public ActorMaterializerSettings WithSupervisionStrategy(Decider decider) /// TBD public ActorMaterializerSettings WithDebugLogging(bool isEnabled) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, isEnabled, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, isEnabled, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -445,7 +448,7 @@ public ActorMaterializerSettings WithDebugLogging(bool isEnabled) /// TBD public ActorMaterializerSettings WithFuzzingMode(bool isFuzzingMode) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, isFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, isFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -455,7 +458,7 @@ public ActorMaterializerSettings WithFuzzingMode(bool isFuzzingMode) /// TBD public ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, isAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, isAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } /// @@ -465,7 +468,7 @@ public ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) /// TBD public ActorMaterializerSettings WithMaxFixedBufferSize(int maxFixedBufferSize) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, maxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, maxFixedBufferSize, SyncProcessingLimit); } /// @@ -475,7 +478,7 @@ public ActorMaterializerSettings WithMaxFixedBufferSize(int maxFixedBufferSize) /// TBD public ActorMaterializerSettings WithSyncProcessingLimit(int limit) { - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, limit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, SubscriptionTimeoutSettings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, limit); } /// @@ -488,7 +491,16 @@ public ActorMaterializerSettings WithSubscriptionTimeoutSettings(StreamSubscript if (Equals(settings, SubscriptionTimeoutSettings)) return this; - return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, settings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, SupervisionDecider, settings, StreamRefSettings, IsDebugLogging, OutputBurstLimit, IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); + } + + public ActorMaterializerSettings WithStreamRefSettings(StreamRefSettings settings) + { + if (settings == null) throw new ArgumentNullException(nameof(settings)); + if (ReferenceEquals(settings, this.StreamRefSettings)) return this; + return new ActorMaterializerSettings(InitialInputBufferSize, MaxInputBufferSize, Dispatcher, + SupervisionDecider, SubscriptionTimeoutSettings, settings, IsDebugLogging, OutputBurstLimit, + IsFuzzingMode, IsAutoFusing, MaxFixedBufferSize, SyncProcessingLimit); } } diff --git a/src/core/Akka.Streams/Akka.Streams.csproj b/src/core/Akka.Streams/Akka.Streams.csproj index 6c5fab2efe6..b3ee8b35d64 100644 --- a/src/core/Akka.Streams/Akka.Streams.csproj +++ b/src/core/Akka.Streams/Akka.Streams.csproj @@ -66,6 +66,7 @@ + diff --git a/src/core/Akka.Streams/Attributes.cs b/src/core/Akka.Streams/Attributes.cs index a265658a277..8a84e5ec36d 100644 --- a/src/core/Akka.Streams/Attributes.cs +++ b/src/core/Akka.Streams/Attributes.cs @@ -471,4 +471,31 @@ public SupervisionStrategy(Decider decider) public static Attributes CreateSupervisionStrategy(Decider strategy) => new Attributes(new SupervisionStrategy(strategy)); } + + /// + /// Attributes for stream refs ( and ). + /// Note that more attributes defined in and . + /// + public static class StreamRefAttributes + { + /// + /// Attributes specific to stream refs. + /// + public interface IStreamRefAttribute : Attributes.IAttribute { } + + public sealed class SubscriptionTimeout : IStreamRefAttribute + { + public TimeSpan Timeout { get; } + + public SubscriptionTimeout(TimeSpan timeout) + { + Timeout = timeout; + } + } + + /// + /// Specifies the subscription timeout within which the remote side MUST subscribe to the handed out stream reference. + /// + public static Attributes CreateSubscriptionTimeout(TimeSpan timeout) => new Attributes(new SubscriptionTimeout(timeout)); + } } diff --git a/src/core/Akka.Streams/Dsl/StreamRefs.cs b/src/core/Akka.Streams/Dsl/StreamRefs.cs new file mode 100644 index 00000000000..50d368018ac --- /dev/null +++ b/src/core/Akka.Streams/Dsl/StreamRefs.cs @@ -0,0 +1,733 @@ +#region copyright +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- +#endregion + +using System; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Annotations; +using Akka.Configuration; +using Akka.Event; +using Akka.Pattern; +using Akka.Streams.Actors; +using Akka.Streams.Dsl; +using Akka.Streams.Implementation; +using Akka.Streams.Stage; +using Akka.Util.Internal; +using Reactive.Streams; + +namespace Akka.Streams.Dsl +{ + + /// + /// API MAY CHANGE: The functionality of stream refs is working, however it is expected that the materialized value + /// will eventually be able to remove the Task wrapping the stream references. For this reason the API is now marked + /// as API may change. See ticket https://github.com/akka/akka/issues/24372 for more details. + /// + /// Factories for creating stream refs. + /// + [ApiMayChange] + public static class StreamRefs + { + /// + /// A local which materializes a which can be used by other streams (including remote ones), + /// to consume data from this local stream, as if they were attached in the spot of the local Sink directly. + /// + /// Adheres to . + /// + /// + [ApiMayChange] + public static Sink>> SourceRef() => + Sink.FromGraph>>(new SinkRefStageImpl(null)); + + /// + /// A local which materializes a which can be used by other streams (including remote ones), + /// to consume data from this local stream, as if they were attached in the spot of the local Sink directly. + /// + /// Adheres to . + /// + /// See more detailed documentation on [[SinkRef]]. + /// + /// + [ApiMayChange] + public static Source>> SinkRef() => + Source.FromGraph>>(new SourceRefStageImpl(null)); + } + + #region StreamRef messages + + internal interface IStreamRefsProtocol { } + + /// + /// Sequenced equivalent. + /// The receiving end of these messages MUST fail the stream if it observes gaps in the sequence, + /// as these messages will not be re-delivered. + /// + /// Sequence numbers start from `0`. + /// + internal sealed class SequencedOnNext : IStreamRefsProtocol, IDeadLetterSuppression + { + public long SeqNr { get; } + public object Payload { get; } + + public SequencedOnNext(long seqNr, object payload) + { + SeqNr = seqNr; + Payload = payload ?? throw ReactiveStreamsCompliance.ElementMustNotBeNullException; + } + } + + /// + /// Initial message sent to remote side to establish partnership between origin and remote stream refs. + /// + internal sealed class OnSubscribeHandshake : IStreamRefsProtocol, IDeadLetterSuppression + { + public OnSubscribeHandshake(IActorRef targetRef) + { + TargetRef = targetRef; + } + + public IActorRef TargetRef { get; } + } + + /// + /// Sent to a the receiver side of a stream ref, once the sending side of the SinkRef gets signalled a Failure. + /// + internal sealed class RemoteStreamFailure : IStreamRefsProtocol + { + public RemoteStreamFailure(string message) + { + Message = message; + } + + public string Message { get; } + } + + /// + /// Sent to a the receiver side of a stream ref, once the sending side of the SinkRef gets signalled a completion. + /// + internal sealed class RemoteStreamCompleted : IStreamRefsProtocol + { + public RemoteStreamCompleted(long seqNr) + { + SeqNr = seqNr; + } + + public long SeqNr { get; } + } + + /// + /// INTERNAL API: Cumulative demand, equivalent to sequence numbering all events in a stream. + /// + /// This message may be re-delivered. + /// + internal sealed class CumulativeDemand : IStreamRefsProtocol, IDeadLetterSuppression + { + public CumulativeDemand(long seqNr) + { + if (seqNr <= 0) throw ReactiveStreamsCompliance.NumberOfElementsInRequestMustBePositiveException; + SeqNr = seqNr; + } + + public long SeqNr { get; } + } + + #endregion + + #region extension + + internal sealed class StreamRefsMaster : IExtension + { + public static StreamRefsMaster Get(ActorSystem system) => + system.WithExtension(); + + private readonly EnumerableActorName sourceRefStageNames = new EnumerableActorNameImpl("SourceRef", new AtomicCounterLong(0L)); + private readonly EnumerableActorName sinkRefStageNames = new EnumerableActorNameImpl("SinkRef", new AtomicCounterLong(0L)); + + public StreamRefsMaster(ExtendedActorSystem system) + { + + } + + public string NextSourceRefName() => sourceRefStageNames.Next(); + public string NextSinkRefName() => sinkRefStageNames.Next(); + } + + internal sealed class StreamRefsMasterProvider : ExtensionIdProvider + { + public override StreamRefsMaster CreateExtension(ExtendedActorSystem system) => + new StreamRefsMaster(system); + } + + #endregion + + public sealed class StreamRefSettings + { + public static StreamRefSettings Create(Config config) + { + if (config == null) throw new ArgumentNullException(nameof(config), "`akka.stream.materializer.stream-ref` was not present"); + + return new StreamRefSettings( + bufferCapacity: config.GetInt("buffer-capacity", 32), + demandRedeliveryInterval: config.GetTimeSpan("demand-redelivery-interval", TimeSpan.FromSeconds(1)), + subscriptionTimeout: config.GetTimeSpan("subscription-timeout", TimeSpan.FromSeconds(30))); + } + + public int BufferCapacity { get; } + public TimeSpan DemandRedeliveryInterval { get; } + public TimeSpan SubscriptionTimeout { get; } + + public StreamRefSettings(int bufferCapacity, TimeSpan demandRedeliveryInterval, TimeSpan subscriptionTimeout) + { + BufferCapacity = bufferCapacity; + DemandRedeliveryInterval = demandRedeliveryInterval; + SubscriptionTimeout = subscriptionTimeout; + } + + public string ProductPrefix => nameof(StreamRefSettings); + + public StreamRefSettings WithBufferCapacity(int value) => Copy(bufferCapacity: value); + public StreamRefSettings WithDemandRedeliveryInterval(TimeSpan value) => Copy(demandRedeliveryInterval: value); + public StreamRefSettings WithSubscriptionTimeout(TimeSpan value) => Copy(subscriptionTimeout: value); + + public StreamRefSettings Copy(int? bufferCapacity = null, + TimeSpan? demandRedeliveryInterval = null, + TimeSpan? subscriptionTimeout = null) => new StreamRefSettings( + bufferCapacity: bufferCapacity ?? this.BufferCapacity, + demandRedeliveryInterval: demandRedeliveryInterval ?? this.DemandRedeliveryInterval, + subscriptionTimeout: subscriptionTimeout ?? this.SubscriptionTimeout); + } + + /// + /// Abstract class defined serialization purposes of . + /// + internal abstract class SourceRefImpl + { + public static SourceRefImpl Create(Type eventType, IActorRef initialPartnerRef) + { + var destType = typeof(SourceRefImpl<>).MakeGenericType(eventType); + return (SourceRefImpl)Activator.CreateInstance(destType, initialPartnerRef); + } + + protected SourceRefImpl(IActorRef initialPartnerRef) + { + InitialPartnerRef = initialPartnerRef; + } + + public IActorRef InitialPartnerRef { get; } + public abstract Type EventType { get; } + } + internal sealed class SourceRefImpl : SourceRefImpl, ISourceRef + { + public SourceRefImpl(IActorRef initialPartnerRef) : base(initialPartnerRef) { } + public override Type EventType => typeof(T); + public Source Source => + Dsl.Source.FromGraph(new SourceRefStageImpl(InitialPartnerRef)).MapMaterializedValue(_ => NotUsed.Instance); + } + + /// + /// Abstract class defined serialization purposes of . + /// + internal abstract class SinkRefImpl + { + public static SinkRefImpl Create(Type eventType, IActorRef initialPartnerRef) + { + var destType = typeof(SinkRefImpl<>).MakeGenericType(eventType); + return (SinkRefImpl)Activator.CreateInstance(destType, initialPartnerRef); + } + + protected SinkRefImpl(IActorRef initialPartnerRef) + { + InitialPartnerRef = initialPartnerRef; + } + + public IActorRef InitialPartnerRef { get; } + public abstract Type EventType { get; } + } + + internal sealed class SinkRefImpl : SinkRefImpl, ISinkRef + { + public SinkRefImpl(IActorRef initialPartnerRef) : base(initialPartnerRef) { } + public override Type EventType => typeof(T); + public Sink Sink => Dsl.Sink.FromGraph(new SinkRefStageImpl(InitialPartnerRef)).MapMaterializedValue(_ => NotUsed.Instance); + } + + /// + /// INTERNAL API: Actual stage implementation backing s. + /// + /// If initialPartnerRef is set, then the remote side is already set up. If it is none, then we are the side creating + /// the ref. + /// + /// + internal sealed class SinkRefStageImpl : GraphStageWithMaterializedValue, Task>> + { + #region logic + + private sealed class Logic : TimerGraphStageLogic, IInHandler + { + private const string SubscriptionTimeoutKey = "SubscriptionTimeoutKey"; + + private readonly SinkRefStageImpl _stage; + private readonly TaskCompletionSource> _promise; + private readonly Attributes _inheritedAttributes; + + private StreamRefsMaster _streamRefsMaster; + private StreamRefSettings _settings; + private StreamRefAttributes.SubscriptionTimeout _subscriptionTimeout; + private string _stageActorName; + + private StreamRefsMaster StreamRefsMaster => _streamRefsMaster ?? (_streamRefsMaster = StreamRefsMaster.Get(ActorMaterializerHelper.Downcast(Materializer).System)); + private StreamRefSettings Settings => _settings ?? (_settings = ActorMaterializerHelper.Downcast(Materializer).Settings.StreamRefSettings); + private StreamRefAttributes.SubscriptionTimeout SubscriptionTimeout => _subscriptionTimeout ?? (_subscriptionTimeout = + _inheritedAttributes.GetAttribute(new StreamRefAttributes.SubscriptionTimeout(Settings.SubscriptionTimeout))); + protected override string StageActorName => _stageActorName ?? (_stageActorName = StreamRefsMaster.NextSinkRefName()); + + private StageActor _stageActor; + + private IActorRef _partnerRef = null; + + #region demand management + private long _remoteCumulativeDemandReceived = 0L; + private long _remoteCumulativeDemandConsumed = 0L; + #endregion + + private Status _completedBeforeRemoteConnected = null; + + public IActorRef Self => _stageActor.Ref; + public IActorRef PartnerRef + { + get + { + if (_partnerRef == null) throw new TargetRefNotInitializedYetException(); + return _partnerRef; + } + } + + public Logic(SinkRefStageImpl stage, TaskCompletionSource> promise, + Attributes inheritedAttributes) : base(stage.Shape) + { + _stage = stage; + _promise = promise; + _inheritedAttributes = inheritedAttributes; + + this.SetHandler(_stage.Inlet, this); + } + + public override void PreStart() + { + _stageActor = GetStageActor(InitialReceive); + var initialPartnerRef = _stage._initialPartnerRef; + if (initialPartnerRef != null) + ObserveAndValidateSender(initialPartnerRef, "Illegal initialPartnerRef! This would be a bug in the SinkRef usage or impl."); + + Log.Debug("Created SinkRef, pointing to remote Sink receiver: {0}, local worker: {1}", initialPartnerRef, Self); + + _promise.SetResult(new SourceRefImpl(Self)); + + if (_partnerRef != null) + { + _partnerRef.Tell(new OnSubscribeHandshake(Self), Self); + TryPull(); + } + + ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); + } + + private void InitialReceive(Tuple args) + { + var sender = args.Item1; + var message = args.Item2; + + switch (message) + { + case Terminated terminated: + if (Equals(terminated.ActorRef, PartnerRef)) + FailStage(new RemoteStreamRefActorTerminatedException($"Remote target receiver of data {PartnerRef} terminated. " + + "Local stream terminating, message loss (on remote side) may have happened.")); + break; + case CumulativeDemand demand: + // the other side may attempt to "double subscribe", which we want to fail eagerly since we're 1:1 pairings + ObserveAndValidateSender(sender, "Illegal sender for CumulativeDemand"); + if (_remoteCumulativeDemandReceived < demand.SeqNr) + { + _remoteCumulativeDemandReceived = demand.SeqNr; + Log.Debug("Received cumulative demand [{0}], consumable demand: [{1}]", demand.SeqNr, _remoteCumulativeDemandReceived - _remoteCumulativeDemandConsumed); + } + TryPull(); + break; + } + } + + public void OnPush() + { + var element = GrabSequenced(_stage.Inlet); + PartnerRef.Tell(element, Self); + Log.Debug("Sending sequenced: {0} to {1}", element, PartnerRef); + TryPull(); + } + + private void TryPull() + { + if (_remoteCumulativeDemandConsumed < _remoteCumulativeDemandReceived && !HasBeenPulled(_stage.Inlet)) + { + Pull(_stage.Inlet); + } + } + + protected internal override void OnTimer(object timerKey) + { + if ((string)timerKey == SubscriptionTimeoutKey) + { + // we know the future has been competed by now, since it is in preStart + var ex = new StreamRefSubscriptionTimeoutException($"[{StageActorName}] Remote side did not subscribe (materialize) handed out Sink reference [${_promise.Task.Result}], " + + "within subscription timeout: ${PrettyDuration.format(subscriptionTimeout.timeout)}!"); + + throw ex; // this will also log the exception, unlike failStage; this should fail rarely, but would be good to have it "loud" + } + } + + private SequencedOnNext GrabSequenced(Inlet inlet) + { + var onNext = new SequencedOnNext(_remoteCumulativeDemandConsumed, Grab(inlet)); + _remoteCumulativeDemandConsumed++; + return onNext; + } + + public void OnUpstreamFailure(Exception cause) + { + if (_partnerRef != null) + { + _partnerRef.Tell(new RemoteStreamFailure(cause.ToString()), Self); + _stageActor.Unwatch(_partnerRef); + FailStage(cause); + } + else + { + _completedBeforeRemoteConnected = new Status.Failure(cause); + // not terminating on purpose, since other side may subscribe still and then we want to fail it + // the stage will be terminated either by timeout, or by the handling in `observeAndValidateSender` + SetKeepGoing(true); + } + } + + public void OnUpstreamFinish() + { + if (_partnerRef != null) + { + _partnerRef.Tell(new RemoteStreamCompleted(_remoteCumulativeDemandConsumed), Self); + _stageActor.Unwatch(_partnerRef); + CompleteStage(); + } + else + { + _completedBeforeRemoteConnected = new Status.Success(Done.Instance); + // not terminating on purpose, since other side may subscribe still and then we want to complete it + SetKeepGoing(true); + } + } + + private void ObserveAndValidateSender(IActorRef partner, string failureMessage) + { + if (_partnerRef == null) + { + _partnerRef = partner; + _stageActor.Watch(_partnerRef); + + switch (_completedBeforeRemoteConnected) + { + case Status.Failure failure: + Log.Warning("Stream already terminated with exception before remote side materialized, failing now."); + partner.Tell(new RemoteStreamFailure(failure.Cause.ToString()), Self); + FailStage(failure.Cause); + break; + case Status.Success _: + Log.Warning("Stream already completed before remote side materialized, failing now."); + partner.Tell(new RemoteStreamCompleted(_remoteCumulativeDemandConsumed), Self); + CompleteStage(); + break; + case null: + if (!Equals(partner, PartnerRef)) + { + var ex = new InvalidPartnerActorException(partner, PartnerRef, failureMessage); + partner.Tell(new RemoteStreamFailure(ex.ToString()), Self); + throw ex; + } + break; + } + } + } + } + + #endregion + + private readonly IActorRef _initialPartnerRef; + + public SinkRefStageImpl(IActorRef initialPartnerRef) + { + _initialPartnerRef = initialPartnerRef; + Shape = new SinkShape(Inlet); + } + + public Inlet Inlet { get; } = new Inlet("SinkRef.in"); + public override SinkShape Shape { get; } + public override ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) + { + var promise = new TaskCompletionSource>(); + return new LogicAndMaterializedValue>>(new Logic(this, promise, inheritedAttributes), promise.Task); + } + } + + /// + /// INTERNAL API: Actual stage implementation backing [[SourceRef]]s. + /// + /// If initialPartnerRef is set, then the remote side is already set up. + /// If it is none, then we are the side creating the ref. + /// + internal sealed class SourceRefStageImpl : GraphStageWithMaterializedValue, Task>> + { + + #region logic + + private sealed class Logic : TimerGraphStageLogic, IOutHandler + { + private const string SubscriptionTimeoutKey = "SubscriptionTimeoutKey"; + private const string DemandRedeliveryTimerKey = "DemandRedeliveryTimerKey"; + + private readonly SourceRefStageImpl _stage; + private readonly TaskCompletionSource> _promise; + private readonly Attributes _inheritedAttributes; + + private StreamRefsMaster _streamRefsMaster; + private StreamRefSettings _settings; + private StreamRefAttributes.SubscriptionTimeout _subscriptionTimeout; + private string _stageActorName; + + private StageActor _stageActor; + private IActorRef _partnerRef = null; + + private StreamRefsMaster StreamRefsMaster => _streamRefsMaster ?? (_streamRefsMaster = StreamRefsMaster.Get(ActorMaterializerHelper.Downcast(Materializer).System)); + private StreamRefSettings Settings => _settings ?? (_settings = ActorMaterializerHelper.Downcast(Materializer).Settings.StreamRefSettings); + private StreamRefAttributes.SubscriptionTimeout SubscriptionTimeout => _subscriptionTimeout ?? (_subscriptionTimeout = + _inheritedAttributes.GetAttribute(new StreamRefAttributes.SubscriptionTimeout(Settings.SubscriptionTimeout))); + protected override string StageActorName => _stageActorName ?? (_stageActorName = StreamRefsMaster.NextSourceRefName()); + + public IActorRef Self => _stageActor.Ref; + public IActorRef PartnerRef + { + get + { + if (_partnerRef == null) throw new TargetRefNotInitializedYetException(); + return _partnerRef; + } + } + + #region demand management + + private bool _completed = false; + private long _expectingSeqNr = 0L; + private long _localCumulativeDemand = 0L; + private long _localRemainingRequested = 0L; + private FixedSizeBuffer _receiveBuffer; // initialized in preStart since depends on settings + private IRequestStrategy _requestStrategy; // initialized in preStart since depends on receiveBuffer's size + #endregion + + public Logic(SourceRefStageImpl stage, TaskCompletionSource> promise, Attributes inheritedAttributes) : base(stage.Shape) + { + _stage = stage; + _promise = promise; + _inheritedAttributes = inheritedAttributes; + + SetHandler(_stage.Outlet, this); + } + + public override void PreStart() + { + _receiveBuffer = new ModuloFixedSizeBuffer(Settings.BufferCapacity); + _requestStrategy = new WatermarkRequestStrategy(highWatermark: _receiveBuffer.Capacity); + + _stageActor = GetStageActor(InitialReceive); + + Log.Debug("[{0}] Allocated receiver: {1}", StageActorName, Self); + + var initialPartnerRef = _stage._initialPartnerRef; + if (initialPartnerRef != null) // this will set the partnerRef + ObserveAndValidateSender(initialPartnerRef, ""); + + _promise.SetResult(new SinkRefImpl(Self)); + + ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); + } + + public void OnPull() + { + TryPush(); + TriggerCumulativeDemand(); + } + + public void OnDownstreamFinish() + { + CompleteStage(); + } + + private void TriggerCumulativeDemand() + { + var i = _receiveBuffer.RemainingCapacity - _localRemainingRequested; + if (_partnerRef != null && i > 0) + { + var addDemand = _requestStrategy.RequestDemand((int)(_receiveBuffer.Used + _localRemainingRequested)); + + // only if demand has increased we shoot it right away + // otherwise it's the same demand level, so it'd be triggered via redelivery anyway + if (addDemand > 0) + { + _localCumulativeDemand += addDemand; + _localRemainingRequested += addDemand; + var demand = new CumulativeDemand(_localCumulativeDemand); + + Log.Debug("[{0}] Demanding until [{1}] (+{2})", _stageActorName, _localCumulativeDemand, addDemand); + PartnerRef.Tell(demand, Self); + ScheduleDemandRedelivery(); + } + } + } + + private void ScheduleDemandRedelivery() => + ScheduleOnce(DemandRedeliveryTimerKey, Settings.DemandRedeliveryInterval); + + protected internal override void OnTimer(object timerKey) + { + switch (timerKey) + { + case SubscriptionTimeoutKey: + var ex = new StreamRefSubscriptionTimeoutException( + // we know the future has been competed by now, since it is in preStart + $"[{StageActorName}] Remote side did not subscribe (materialize) handed out Sink reference [{_promise.Task.Result}]," + + $"within subscription timeout: {SubscriptionTimeout.Timeout}!"); + throw ex; + case DemandRedeliveryTimerKey: + Log.Debug("[{0}] Scheduled re-delivery of demand until [{1}]", StageActorName, _localCumulativeDemand); + PartnerRef.Tell(new CumulativeDemand(_localCumulativeDemand), Self); + ScheduleDemandRedelivery(); + break; + } + } + + private void InitialReceive(Tuple args) + { + var sender = args.Item1; + var message = args.Item2; + + switch (message) + { + case OnSubscribeHandshake handshake: + CancelTimer(SubscriptionTimeoutKey); + ObserveAndValidateSender(sender, "Illegal sender in OnSubscribeHandshake"); + Log.Debug("[{0}] Received handshake {1} from {2}", StageActorName, message, sender); + TriggerCumulativeDemand(); + break; + case SequencedOnNext onNext: + ObserveAndValidateSender(sender, "Illegal sender in SequencedOnNext"); + ObserveAndValidateSequenceNr(onNext.SeqNr, "Illegal sequence nr in SequencedOnNext"); + Log.Debug("[{0}] Received seq {1} from {2}", StageActorName, message, sender); + OnReceiveElement(onNext.Payload); + TriggerCumulativeDemand(); + break; + case RemoteStreamCompleted completed: + ObserveAndValidateSender(sender, "Illegal sender in RemoteStreamCompleted"); + ObserveAndValidateSequenceNr(completed.SeqNr, "Illegal sequence nr in RemoteStreamCompleted"); + Log.Debug("[{0}] The remote stream has completed, completing as well...", StageActorName); + _stageActor.Unwatch(sender); + _completed = true; + TryPush(); + break; + case RemoteStreamFailure failure: + ObserveAndValidateSender(sender, "Illegal sender in RemoteStreamFailure"); + Log.Warning("[{0}] The remote stream has failed, failing (reason: {1})", StageActorName, failure.Message); + _stageActor.Unwatch(sender); + FailStage(new RemoteStreamRefActorTerminatedException($"Remote stream ({sender.Path}) failed, reason: {failure.Message}")); + break; + case Terminated terminated: + if (Equals(_partnerRef, terminated.ActorRef)) + FailStage(new RemoteStreamRefActorTerminatedException( + $"The remote partner {terminated.ActorRef} has terminated! Tearing down this side of the stream as well.")); + else + FailStage(new RemoteStreamRefActorTerminatedException( + $"Received UNEXPECTED Terminated({terminated.ActorRef}) message! This actor was NOT our trusted remote partner, which was: {_partnerRef}. Tearing down.")); + + break; + } + } + + private void TryPush() + { + if (!_receiveBuffer.IsEmpty && IsAvailable(_stage.Outlet)) Push(_stage.Outlet, _receiveBuffer.Dequeue()); + else if (_receiveBuffer.IsEmpty && _completed) CompleteStage(); + } + + private void OnReceiveElement(object payload) + { + var outlet = _stage.Outlet; + _localRemainingRequested--; + if (_receiveBuffer.IsEmpty && IsAvailable(outlet)) + Push(outlet, (TOut)payload); + else if (_receiveBuffer.IsFull) + throw new IllegalStateException($"Attempted to overflow buffer! Capacity: {_receiveBuffer.Capacity}, incoming element: {payload}, localRemainingRequested: {_localRemainingRequested}, localCumulativeDemand: {_localCumulativeDemand}"); + else + _receiveBuffer.Enqueue((TOut)payload); + } + + /// + /// TBD + /// + /// Thrown when is invalid + private void ObserveAndValidateSender(IActorRef partner, string failureMessage) + { + if (_partnerRef == null) + { + Log.Debug("Received first message from {0}, assuming it to be the remote partner for this stage", partner); + _partnerRef = partner; + _stageActor.Watch(partner); + } + else if (!Equals(_partnerRef, partner)) + { + var ex = new InvalidPartnerActorException(partner, PartnerRef, failureMessage); + partner.Tell(new RemoteStreamFailure(ex.Message), Self); + throw ex; + } + } + + private void ObserveAndValidateSequenceNr(long seqNr, string failureMessage) + { + if (seqNr != _expectingSeqNr) + throw new InvalidSequenceNumberException(_expectingSeqNr, seqNr, failureMessage); + else + _expectingSeqNr++; + } + } + + #endregion + + private readonly IActorRef _initialPartnerRef; + + public SourceRefStageImpl(IActorRef initialPartnerRef) + { + _initialPartnerRef = initialPartnerRef; + + Shape = new SourceShape(Outlet); + } + + public Outlet Outlet { get; } = new Outlet("SourceRef.out"); + public override SourceShape Shape { get; } + + public override ILogicAndMaterializedValue>> CreateLogicAndMaterializedValue(Attributes inheritedAttributes) + { + var promise = new TaskCompletionSource>(); + return new LogicAndMaterializedValue>>(new Logic(this, promise, inheritedAttributes), promise.Task); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/Implementation/Buffers.cs b/src/core/Akka.Streams/Implementation/Buffers.cs index cec8295d7ef..ba08816131e 100644 --- a/src/core/Akka.Streams/Implementation/Buffers.cs +++ b/src/core/Akka.Streams/Implementation/Buffers.cs @@ -194,6 +194,8 @@ protected FixedSizeBuffer(int capacity) /// public bool NonEmpty => Used != 0; + public long RemainingCapacity => Capacity - Used; + // for the maintenance parameter see dropHead /// /// TBD diff --git a/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs b/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs new file mode 100644 index 00000000000..c78ae146726 --- /dev/null +++ b/src/core/Akka.Streams/Serialization/Proto/StreamRefMessages.g.cs @@ -0,0 +1,1709 @@ +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: StreamRefMessages.proto +#pragma warning disable 1591, 0612, 3021 +#region Designer generated code + +using pb = global::Google.Protobuf; +using pbc = global::Google.Protobuf.Collections; +using pbr = global::Google.Protobuf.Reflection; +using scg = global::System.Collections.Generic; +namespace Akka.Streams.Serialization.Proto.Msg +{ + + /// Holder for reflection information generated from StreamRefMessages.proto + internal static partial class StreamRefMessagesReflection + { + + #region Descriptor + /// File descriptor for StreamRefMessages.proto + public static pbr::FileDescriptor Descriptor + { + get { return descriptor; } + } + private static pbr::FileDescriptor descriptor; + + static StreamRefMessagesReflection() + { + byte[] descriptorData = global::System.Convert.FromBase64String( + string.Concat( + "ChdTdHJlYW1SZWZNZXNzYWdlcy5wcm90bxIkQWtrYS5TdHJlYW1zLlNlcmlh", + "bGl6YXRpb24uUHJvdG8uTXNnIh0KCUV2ZW50VHlwZRIQCgh0eXBlTmFtZRgB", + "IAEoCSKQAQoHU2lua1JlZhJBCgl0YXJnZXRSZWYYASABKAsyLi5Ba2thLlN0", + "cmVhbXMuU2VyaWFsaXphdGlvbi5Qcm90by5Nc2cuQWN0b3JSZWYSQgoJZXZl", + "bnRUeXBlGAIgASgLMi8uQWtrYS5TdHJlYW1zLlNlcmlhbGl6YXRpb24uUHJv", + "dG8uTXNnLkV2ZW50VHlwZSKSAQoJU291cmNlUmVmEkEKCW9yaWdpblJlZhgB", + "IAEoCzIuLkFra2EuU3RyZWFtcy5TZXJpYWxpemF0aW9uLlByb3RvLk1zZy5B", + "Y3RvclJlZhJCCglldmVudFR5cGUYAiABKAsyLy5Ba2thLlN0cmVhbXMuU2Vy", + "aWFsaXphdGlvbi5Qcm90by5Nc2cuRXZlbnRUeXBlIhgKCEFjdG9yUmVmEgwK", + "BHBhdGgYASABKAkiUQoHUGF5bG9hZBIXCg9lbmNsb3NlZE1lc3NhZ2UYASAB", + "KAwSFAoMc2VyaWFsaXplcklkGAIgASgFEhcKD21lc3NhZ2VNYW5pZmVzdBgD", + "IAEoDCJZChRPblN1YnNjcmliZUhhbmRzaGFrZRJBCgl0YXJnZXRSZWYYASAB", + "KAsyLi5Ba2thLlN0cmVhbXMuU2VyaWFsaXphdGlvbi5Qcm90by5Nc2cuQWN0", + "b3JSZWYiIQoQQ3VtdWxhdGl2ZURlbWFuZBINCgVzZXFOchgBIAEoAyJgCg9T", + "ZXF1ZW5jZWRPbk5leHQSDQoFc2VxTnIYASABKAMSPgoHcGF5bG9hZBgCIAEo", + "CzItLkFra2EuU3RyZWFtcy5TZXJpYWxpemF0aW9uLlByb3RvLk1zZy5QYXls", + "b2FkIiQKE1JlbW90ZVN0cmVhbUZhaWx1cmUSDQoFY2F1c2UYASABKAwiJgoV", + "UmVtb3RlU3RyZWFtQ29tcGxldGVkEg0KBXNlcU5yGAEgASgDQgJIAWIGcHJv", + "dG8z")); + descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, + new pbr::FileDescriptor[] { }, + new pbr::GeneratedClrTypeInfo(null, new pbr::GeneratedClrTypeInfo[] { + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.EventType), global::Akka.Streams.Serialization.Proto.Msg.EventType.Parser, new[]{ "TypeName" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SinkRef), global::Akka.Streams.Serialization.Proto.Msg.SinkRef.Parser, new[]{ "TargetRef", "EventType" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SourceRef), global::Akka.Streams.Serialization.Proto.Msg.SourceRef.Parser, new[]{ "OriginRef", "EventType" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.ActorRef), global::Akka.Streams.Serialization.Proto.Msg.ActorRef.Parser, new[]{ "Path" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.Payload), global::Akka.Streams.Serialization.Proto.Msg.Payload.Parser, new[]{ "EnclosedMessage", "SerializerId", "MessageManifest" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.OnSubscribeHandshake), global::Akka.Streams.Serialization.Proto.Msg.OnSubscribeHandshake.Parser, new[]{ "TargetRef" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.CumulativeDemand), global::Akka.Streams.Serialization.Proto.Msg.CumulativeDemand.Parser, new[]{ "SeqNr" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.SequencedOnNext), global::Akka.Streams.Serialization.Proto.Msg.SequencedOnNext.Parser, new[]{ "SeqNr", "Payload" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamFailure), global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamFailure.Parser, new[]{ "Cause" }, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamCompleted), global::Akka.Streams.Serialization.Proto.Msg.RemoteStreamCompleted.Parser, new[]{ "SeqNr" }, null, null, null) + })); + } + #endregion + + } + #region Messages + internal sealed partial class EventType : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new EventType()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[0]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public EventType() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public EventType(EventType other) : this() + { + typeName_ = other.typeName_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public EventType Clone() + { + return new EventType(this); + } + + /// Field number for the "typeName" field. + public const int TypeNameFieldNumber = 1; + private string typeName_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string TypeName + { + get { return typeName_; } + set + { + typeName_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as EventType); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(EventType other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (TypeName != other.TypeName) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (TypeName.Length != 0) hash ^= TypeName.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (TypeName.Length != 0) + { + output.WriteRawTag(10); + output.WriteString(TypeName); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (TypeName.Length != 0) + { + size += 1 + pb::CodedOutputStream.ComputeStringSize(TypeName); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(EventType other) + { + if (other == null) + { + return; + } + if (other.TypeName.Length != 0) + { + TypeName = other.TypeName; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 10: + { + TypeName = input.ReadString(); + break; + } + } + } + } + + } + + internal sealed partial class SinkRef : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SinkRef()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[1]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SinkRef() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SinkRef(SinkRef other) : this() + { + TargetRef = other.targetRef_ != null ? other.TargetRef.Clone() : null; + EventType = other.eventType_ != null ? other.EventType.Clone() : null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SinkRef Clone() + { + return new SinkRef(this); + } + + /// Field number for the "targetRef" field. + public const int TargetRefFieldNumber = 1; + private global::Akka.Streams.Serialization.Proto.Msg.ActorRef targetRef_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.ActorRef TargetRef + { + get { return targetRef_; } + set + { + targetRef_ = value; + } + } + + /// Field number for the "eventType" field. + public const int EventTypeFieldNumber = 2; + private global::Akka.Streams.Serialization.Proto.Msg.EventType eventType_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.EventType EventType + { + get { return eventType_; } + set + { + eventType_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as SinkRef); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(SinkRef other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (!object.Equals(TargetRef, other.TargetRef)) return false; + if (!object.Equals(EventType, other.EventType)) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (targetRef_ != null) hash ^= TargetRef.GetHashCode(); + if (eventType_ != null) hash ^= EventType.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (targetRef_ != null) + { + output.WriteRawTag(10); + output.WriteMessage(TargetRef); + } + if (eventType_ != null) + { + output.WriteRawTag(18); + output.WriteMessage(EventType); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (targetRef_ != null) + { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(TargetRef); + } + if (eventType_ != null) + { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(EventType); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(SinkRef other) + { + if (other == null) + { + return; + } + if (other.targetRef_ != null) + { + if (targetRef_ == null) + { + targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + TargetRef.MergeFrom(other.TargetRef); + } + if (other.eventType_ != null) + { + if (eventType_ == null) + { + eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); + } + EventType.MergeFrom(other.EventType); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 10: + { + if (targetRef_ == null) + { + targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + input.ReadMessage(targetRef_); + break; + } + case 18: + { + if (eventType_ == null) + { + eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); + } + input.ReadMessage(eventType_); + break; + } + } + } + } + + } + + internal sealed partial class SourceRef : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SourceRef()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[2]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SourceRef() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SourceRef(SourceRef other) : this() + { + OriginRef = other.originRef_ != null ? other.OriginRef.Clone() : null; + EventType = other.eventType_ != null ? other.EventType.Clone() : null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SourceRef Clone() + { + return new SourceRef(this); + } + + /// Field number for the "originRef" field. + public const int OriginRefFieldNumber = 1; + private global::Akka.Streams.Serialization.Proto.Msg.ActorRef originRef_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.ActorRef OriginRef + { + get { return originRef_; } + set + { + originRef_ = value; + } + } + + /// Field number for the "eventType" field. + public const int EventTypeFieldNumber = 2; + private global::Akka.Streams.Serialization.Proto.Msg.EventType eventType_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.EventType EventType + { + get { return eventType_; } + set + { + eventType_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as SourceRef); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(SourceRef other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (!object.Equals(OriginRef, other.OriginRef)) return false; + if (!object.Equals(EventType, other.EventType)) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (originRef_ != null) hash ^= OriginRef.GetHashCode(); + if (eventType_ != null) hash ^= EventType.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (originRef_ != null) + { + output.WriteRawTag(10); + output.WriteMessage(OriginRef); + } + if (eventType_ != null) + { + output.WriteRawTag(18); + output.WriteMessage(EventType); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (originRef_ != null) + { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(OriginRef); + } + if (eventType_ != null) + { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(EventType); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(SourceRef other) + { + if (other == null) + { + return; + } + if (other.originRef_ != null) + { + if (originRef_ == null) + { + originRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + OriginRef.MergeFrom(other.OriginRef); + } + if (other.eventType_ != null) + { + if (eventType_ == null) + { + eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); + } + EventType.MergeFrom(other.EventType); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 10: + { + if (originRef_ == null) + { + originRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + input.ReadMessage(originRef_); + break; + } + case 18: + { + if (eventType_ == null) + { + eventType_ = new global::Akka.Streams.Serialization.Proto.Msg.EventType(); + } + input.ReadMessage(eventType_); + break; + } + } + } + } + + } + + internal sealed partial class ActorRef : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ActorRef()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[3]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public ActorRef() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public ActorRef(ActorRef other) : this() + { + path_ = other.path_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public ActorRef Clone() + { + return new ActorRef(this); + } + + /// Field number for the "path" field. + public const int PathFieldNumber = 1; + private string path_ = ""; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public string Path + { + get { return path_; } + set + { + path_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as ActorRef); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(ActorRef other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (Path != other.Path) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (Path.Length != 0) hash ^= Path.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (Path.Length != 0) + { + output.WriteRawTag(10); + output.WriteString(Path); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (Path.Length != 0) + { + size += 1 + pb::CodedOutputStream.ComputeStringSize(Path); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(ActorRef other) + { + if (other == null) + { + return; + } + if (other.Path.Length != 0) + { + Path = other.Path; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 10: + { + Path = input.ReadString(); + break; + } + } + } + } + + } + + internal sealed partial class Payload : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new Payload()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[4]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public Payload() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public Payload(Payload other) : this() + { + enclosedMessage_ = other.enclosedMessage_; + serializerId_ = other.serializerId_; + messageManifest_ = other.messageManifest_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public Payload Clone() + { + return new Payload(this); + } + + /// Field number for the "enclosedMessage" field. + public const int EnclosedMessageFieldNumber = 1; + private pb::ByteString enclosedMessage_ = pb::ByteString.Empty; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public pb::ByteString EnclosedMessage + { + get { return enclosedMessage_; } + set + { + enclosedMessage_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + /// Field number for the "serializerId" field. + public const int SerializerIdFieldNumber = 2; + private int serializerId_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int SerializerId + { + get { return serializerId_; } + set + { + serializerId_ = value; + } + } + + /// Field number for the "messageManifest" field. + public const int MessageManifestFieldNumber = 3; + private pb::ByteString messageManifest_ = pb::ByteString.Empty; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public pb::ByteString MessageManifest + { + get { return messageManifest_; } + set + { + messageManifest_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as Payload); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(Payload other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (EnclosedMessage != other.EnclosedMessage) return false; + if (SerializerId != other.SerializerId) return false; + if (MessageManifest != other.MessageManifest) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (EnclosedMessage.Length != 0) hash ^= EnclosedMessage.GetHashCode(); + if (SerializerId != 0) hash ^= SerializerId.GetHashCode(); + if (MessageManifest.Length != 0) hash ^= MessageManifest.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (EnclosedMessage.Length != 0) + { + output.WriteRawTag(10); + output.WriteBytes(EnclosedMessage); + } + if (SerializerId != 0) + { + output.WriteRawTag(16); + output.WriteInt32(SerializerId); + } + if (MessageManifest.Length != 0) + { + output.WriteRawTag(26); + output.WriteBytes(MessageManifest); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (EnclosedMessage.Length != 0) + { + size += 1 + pb::CodedOutputStream.ComputeBytesSize(EnclosedMessage); + } + if (SerializerId != 0) + { + size += 1 + pb::CodedOutputStream.ComputeInt32Size(SerializerId); + } + if (MessageManifest.Length != 0) + { + size += 1 + pb::CodedOutputStream.ComputeBytesSize(MessageManifest); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(Payload other) + { + if (other == null) + { + return; + } + if (other.EnclosedMessage.Length != 0) + { + EnclosedMessage = other.EnclosedMessage; + } + if (other.SerializerId != 0) + { + SerializerId = other.SerializerId; + } + if (other.MessageManifest.Length != 0) + { + MessageManifest = other.MessageManifest; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 10: + { + EnclosedMessage = input.ReadBytes(); + break; + } + case 16: + { + SerializerId = input.ReadInt32(); + break; + } + case 26: + { + MessageManifest = input.ReadBytes(); + break; + } + } + } + } + + } + + internal sealed partial class OnSubscribeHandshake : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new OnSubscribeHandshake()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[5]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public OnSubscribeHandshake() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public OnSubscribeHandshake(OnSubscribeHandshake other) : this() + { + TargetRef = other.targetRef_ != null ? other.TargetRef.Clone() : null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public OnSubscribeHandshake Clone() + { + return new OnSubscribeHandshake(this); + } + + /// Field number for the "targetRef" field. + public const int TargetRefFieldNumber = 1; + private global::Akka.Streams.Serialization.Proto.Msg.ActorRef targetRef_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.ActorRef TargetRef + { + get { return targetRef_; } + set + { + targetRef_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as OnSubscribeHandshake); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(OnSubscribeHandshake other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (!object.Equals(TargetRef, other.TargetRef)) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (targetRef_ != null) hash ^= TargetRef.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (targetRef_ != null) + { + output.WriteRawTag(10); + output.WriteMessage(TargetRef); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (targetRef_ != null) + { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(TargetRef); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(OnSubscribeHandshake other) + { + if (other == null) + { + return; + } + if (other.targetRef_ != null) + { + if (targetRef_ == null) + { + targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + TargetRef.MergeFrom(other.TargetRef); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 10: + { + if (targetRef_ == null) + { + targetRef_ = new global::Akka.Streams.Serialization.Proto.Msg.ActorRef(); + } + input.ReadMessage(targetRef_); + break; + } + } + } + } + + } + + internal sealed partial class CumulativeDemand : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new CumulativeDemand()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[6]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public CumulativeDemand() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public CumulativeDemand(CumulativeDemand other) : this() + { + seqNr_ = other.seqNr_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public CumulativeDemand Clone() + { + return new CumulativeDemand(this); + } + + /// Field number for the "seqNr" field. + public const int SeqNrFieldNumber = 1; + private long seqNr_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public long SeqNr + { + get { return seqNr_; } + set + { + seqNr_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as CumulativeDemand); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(CumulativeDemand other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (SeqNr != other.SeqNr) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (SeqNr != 0L) + { + output.WriteRawTag(8); + output.WriteInt64(SeqNr); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (SeqNr != 0L) + { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(CumulativeDemand other) + { + if (other == null) + { + return; + } + if (other.SeqNr != 0L) + { + SeqNr = other.SeqNr; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 8: + { + SeqNr = input.ReadInt64(); + break; + } + } + } + } + + } + + internal sealed partial class SequencedOnNext : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new SequencedOnNext()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[7]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SequencedOnNext() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SequencedOnNext(SequencedOnNext other) : this() + { + seqNr_ = other.seqNr_; + Payload = other.payload_ != null ? other.Payload.Clone() : null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public SequencedOnNext Clone() + { + return new SequencedOnNext(this); + } + + /// Field number for the "seqNr" field. + public const int SeqNrFieldNumber = 1; + private long seqNr_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public long SeqNr + { + get { return seqNr_; } + set + { + seqNr_ = value; + } + } + + /// Field number for the "payload" field. + public const int PayloadFieldNumber = 2; + private global::Akka.Streams.Serialization.Proto.Msg.Payload payload_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public global::Akka.Streams.Serialization.Proto.Msg.Payload Payload + { + get { return payload_; } + set + { + payload_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as SequencedOnNext); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(SequencedOnNext other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (SeqNr != other.SeqNr) return false; + if (!object.Equals(Payload, other.Payload)) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); + if (payload_ != null) hash ^= Payload.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (SeqNr != 0L) + { + output.WriteRawTag(8); + output.WriteInt64(SeqNr); + } + if (payload_ != null) + { + output.WriteRawTag(18); + output.WriteMessage(Payload); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (SeqNr != 0L) + { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); + } + if (payload_ != null) + { + size += 1 + pb::CodedOutputStream.ComputeMessageSize(Payload); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(SequencedOnNext other) + { + if (other == null) + { + return; + } + if (other.SeqNr != 0L) + { + SeqNr = other.SeqNr; + } + if (other.payload_ != null) + { + if (payload_ == null) + { + payload_ = new global::Akka.Streams.Serialization.Proto.Msg.Payload(); + } + Payload.MergeFrom(other.Payload); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 8: + { + SeqNr = input.ReadInt64(); + break; + } + case 18: + { + if (payload_ == null) + { + payload_ = new global::Akka.Streams.Serialization.Proto.Msg.Payload(); + } + input.ReadMessage(payload_); + break; + } + } + } + } + + } + + internal sealed partial class RemoteStreamFailure : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new RemoteStreamFailure()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[8]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamFailure() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamFailure(RemoteStreamFailure other) : this() + { + cause_ = other.cause_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamFailure Clone() + { + return new RemoteStreamFailure(this); + } + + /// Field number for the "cause" field. + public const int CauseFieldNumber = 1; + private pb::ByteString cause_ = pb::ByteString.Empty; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public pb::ByteString Cause + { + get { return cause_; } + set + { + cause_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as RemoteStreamFailure); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(RemoteStreamFailure other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (Cause != other.Cause) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (Cause.Length != 0) hash ^= Cause.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (Cause.Length != 0) + { + output.WriteRawTag(10); + output.WriteBytes(Cause); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (Cause.Length != 0) + { + size += 1 + pb::CodedOutputStream.ComputeBytesSize(Cause); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(RemoteStreamFailure other) + { + if (other == null) + { + return; + } + if (other.Cause.Length != 0) + { + Cause = other.Cause; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 10: + { + Cause = input.ReadBytes(); + break; + } + } + } + } + + } + + internal sealed partial class RemoteStreamCompleted : pb::IMessage + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new RemoteStreamCompleted()); + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public static pbr::MessageDescriptor Descriptor + { + get { return global::Akka.Streams.Serialization.Proto.Msg.StreamRefMessagesReflection.Descriptor.MessageTypes[9]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + pbr::MessageDescriptor pb::IMessage.Descriptor + { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamCompleted() + { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamCompleted(RemoteStreamCompleted other) : this() + { + seqNr_ = other.seqNr_; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public RemoteStreamCompleted Clone() + { + return new RemoteStreamCompleted(this); + } + + /// Field number for the "seqNr" field. + public const int SeqNrFieldNumber = 1; + private long seqNr_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public long SeqNr + { + get { return seqNr_; } + set + { + seqNr_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override bool Equals(object other) + { + return Equals(other as RemoteStreamCompleted); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public bool Equals(RemoteStreamCompleted other) + { + if (ReferenceEquals(other, null)) + { + return false; + } + if (ReferenceEquals(other, this)) + { + return true; + } + if (SeqNr != other.SeqNr) return false; + return true; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override int GetHashCode() + { + int hash = 1; + if (SeqNr != 0L) hash ^= SeqNr.GetHashCode(); + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public override string ToString() + { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void WriteTo(pb::CodedOutputStream output) + { + if (SeqNr != 0L) + { + output.WriteRawTag(8); + output.WriteInt64(SeqNr); + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public int CalculateSize() + { + int size = 0; + if (SeqNr != 0L) + { + size += 1 + pb::CodedOutputStream.ComputeInt64Size(SeqNr); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(RemoteStreamCompleted other) + { + if (other == null) + { + return; + } + if (other.SeqNr != 0L) + { + SeqNr = other.SeqNr; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + public void MergeFrom(pb::CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) + { + switch (tag) + { + default: + input.SkipLastField(); + break; + case 8: + { + SeqNr = input.ReadInt64(); + break; + } + } + } + } + + } + + #endregion + +} + +#endregion Designer generated code \ No newline at end of file diff --git a/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs b/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs new file mode 100644 index 00000000000..8d823ba46d2 --- /dev/null +++ b/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs @@ -0,0 +1,237 @@ +#region copyright +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- +#endregion + +using System; +using System.Text; +using Akka.Actor; +using Akka.Serialization; +using Akka.Streams.Implementation; +using Akka.Streams.Serialization.Proto.Msg; +using Akka.Util; +using Google.Protobuf; +using Akka.Streams.Dsl; +using CumulativeDemand = Akka.Streams.Dsl.CumulativeDemand; +using OnSubscribeHandshake = Akka.Streams.Dsl.OnSubscribeHandshake; +using RemoteStreamCompleted = Akka.Streams.Dsl.RemoteStreamCompleted; +using RemoteStreamFailure = Akka.Streams.Dsl.RemoteStreamFailure; +using SequencedOnNext = Akka.Streams.Dsl.SequencedOnNext; + +namespace Akka.Streams.Serialization +{ + public sealed class StreamRefSerializer : SerializerWithStringManifest + { + private readonly ExtendedActorSystem _system; + private readonly Akka.Serialization.Serialization _serialization; + + private const string SequencedOnNextManifest = "A"; + private const string CumulativeDemandManifest = "B"; + private const string RemoteSinkFailureManifest = "C"; + private const string RemoteSinkCompletedManifest = "D"; + private const string SourceRefManifest = "E"; + private const string SinkRefManifest = "F"; + private const string OnSubscribeHandshakeManifest = "G"; + + public StreamRefSerializer(ExtendedActorSystem system) : base(system) + { + _system = system; + _serialization = system.Serialization; + } + + public override string Manifest(object o) + { + switch (o) + { + case SequencedOnNext _: return SequencedOnNextManifest; + case CumulativeDemand _: return CumulativeDemandManifest; + case OnSubscribeHandshake _: return OnSubscribeHandshakeManifest; + case RemoteStreamFailure _: return RemoteSinkFailureManifest; + case RemoteStreamCompleted _: return RemoteSinkCompletedManifest; + case SourceRefImpl _: return SourceRefManifest; + case SinkRefImpl _: return SinkRefManifest; + default: throw new ArgumentException($"Unsupported object of type {o.GetType()}", nameof(o)); + } + } + + public override byte[] ToBinary(object o) + { + switch (o) + { + case SequencedOnNext onNext: return SerializeSequencedOnNext(onNext).ToByteArray(); + case CumulativeDemand demand: return SerializeCumulativeDemand(demand).ToByteArray(); + case OnSubscribeHandshake handshake: return SerializeOnSubscribeHandshake(handshake).ToByteArray(); + case RemoteStreamFailure failure: return SerializeRemoteStreamFailure(failure).ToByteArray(); + case RemoteStreamCompleted completed: return SerializeRemoteStreamCompleted(completed).ToByteArray(); + case SourceRefImpl sourceRef: return SerializeSourceRef(sourceRef).ToByteArray(); + case SinkRefImpl sinkRef: return SerializeSinkRef(sinkRef).ToByteArray(); + default: throw new ArgumentException($"Unsupported object of type {o.GetType()}", nameof(o)); + } + } + + public override object FromBinary(byte[] bytes, string manifest) + { + switch (manifest) + { + case SequencedOnNextManifest: return DeserializeSequenceOnNext(bytes); + case CumulativeDemandManifest: return DeserializeCumulativeDemand(bytes); + case OnSubscribeHandshakeManifest: return DeserializeOnSubscribeHandshake(bytes); + case RemoteSinkFailureManifest: return DeserializeRemoteSinkFailure(bytes); + case RemoteSinkCompletedManifest: return DeserializeRemoteSinkCompleted(bytes); + case SourceRefManifest: return DeserializeSourceRef(bytes); + case SinkRefManifest: return DeserializeSinkRef(bytes); + default: throw new ArgumentException($"Unsupported manifest '{manifest}'", nameof(manifest)); + } + } + + private Type TypeFromProto(Proto.Msg.EventType eventType) + { + var typeName = eventType.TypeName; + return Type.GetType(typeName, throwOnError: true); + } + + private Proto.Msg.EventType TypeToProto(Type clrType) => new Proto.Msg.EventType + { + TypeName = clrType.TypeQualifiedName() + }; + + private SinkRefImpl DeserializeSinkRef(byte[] bytes) + { + var sinkRef = SinkRef.Parser.ParseFrom(bytes); + var type = TypeFromProto(sinkRef.EventType); + var targetRef = _system.Provider.ResolveActorRef(sinkRef.TargetRef.Path); + return SinkRefImpl.Create(type, targetRef); + } + + private SourceRefImpl DeserializeSourceRef(byte[] bytes) + { + var sourceRef = SourceRef.Parser.ParseFrom(bytes); + var type = TypeFromProto(sourceRef.EventType); + var originRef = _system.Provider.ResolveActorRef(sourceRef.OriginRef.Path); + return SourceRefImpl.Create(type, originRef); + } + + private RemoteStreamCompleted DeserializeRemoteSinkCompleted(byte[] bytes) + { + var completed = Proto.Msg.RemoteStreamCompleted.Parser.ParseFrom(bytes); + return new RemoteStreamCompleted(completed.SeqNr); + } + + private RemoteStreamFailure DeserializeRemoteSinkFailure(byte[] bytes) + { + var failure = Proto.Msg.RemoteStreamFailure.Parser.ParseFrom(bytes); + var errorMessage = Encoding.UTF8.GetString(failure.Cause.ToByteArray()); + return new RemoteStreamFailure(errorMessage); + } + + private OnSubscribeHandshake DeserializeOnSubscribeHandshake(byte[] bytes) + { + var handshake = Proto.Msg.OnSubscribeHandshake.Parser.ParseFrom(bytes); + var targetRef = _system.Provider.ResolveActorRef(handshake.TargetRef.Path); + return new OnSubscribeHandshake(targetRef); + } + + private CumulativeDemand DeserializeCumulativeDemand(byte[] bytes) + { + var demand = Proto.Msg.CumulativeDemand.Parser.ParseFrom(bytes); + return new CumulativeDemand(demand.SeqNr); + } + + private SequencedOnNext DeserializeSequenceOnNext(byte[] bytes) + { + var onNext = Proto.Msg.SequencedOnNext.Parser.ParseFrom(bytes); + var serializer = _serialization.GetSerializerById(onNext.Payload.SerializerId); + object payload; + if (onNext.Payload.MessageManifest != null) + { + var manifest = Encoding.UTF8.GetString(onNext.Payload.MessageManifest.ToByteArray()); + if (serializer is SerializerWithStringManifest s) + { + payload = s.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), manifest); + } + else + { + var type = Type.GetType(manifest, throwOnError: true); + payload = serializer.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), type); + } + } + else + { + payload = serializer.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), null); + } + + return new SequencedOnNext(onNext.SeqNr, payload); + } + + private ByteString SerializeSinkRef(SinkRefImpl sinkRef) => new SinkRef + { + EventType = TypeToProto(sinkRef.EventType), + TargetRef = new ActorRef + { + Path = Akka.Serialization.Serialization.SerializedActorPath(sinkRef.InitialPartnerRef) + } + }.ToByteString(); + + private ByteString SerializeSourceRef(SourceRefImpl sourceRef) + { + return new SourceRef + { + EventType = TypeToProto(sourceRef.EventType), + OriginRef = new ActorRef + { + Path = Akka.Serialization.Serialization.SerializedActorPath(sourceRef.InitialPartnerRef) + } + }.ToByteString(); + } + + private ByteString SerializeRemoteStreamCompleted(RemoteStreamCompleted completed) => + new Proto.Msg.RemoteStreamCompleted { SeqNr = completed.SeqNr }.ToByteString(); + + private ByteString SerializeRemoteStreamFailure(RemoteStreamFailure failure) => new Proto.Msg.RemoteStreamFailure + { + Cause = ByteString.CopyFromUtf8(failure.Message) + }.ToByteString(); + + private ByteString SerializeOnSubscribeHandshake(OnSubscribeHandshake handshake) => + new Proto.Msg.OnSubscribeHandshake + { + TargetRef = new ActorRef + { Path = Akka.Serialization.Serialization.SerializedActorPath(handshake.TargetRef) } + }.ToByteString(); + + private ByteString SerializeCumulativeDemand(CumulativeDemand demand) => + new Proto.Msg.CumulativeDemand { SeqNr = demand.SeqNr }.ToByteString(); + + private ByteString SerializeSequencedOnNext(SequencedOnNext onNext) + { + var payload = onNext.Payload; + var serializer = _serialization.FindSerializerFor(payload); + string manifest = null; + if (serializer.IncludeManifest) + { + manifest = serializer is SerializerWithStringManifest s + ? s.Manifest(payload) + : payload.GetType().TypeQualifiedName(); + } + + var p = new Payload + { + EnclosedMessage = ByteString.CopyFrom(serializer.ToBinary(payload)), + SerializerId = serializer.Identifier + }; + + if (!string.IsNullOrEmpty(manifest)) + p.MessageManifest = ByteString.CopyFromUtf8(manifest); + + return new Proto.Msg.SequencedOnNext + { + SeqNr = onNext.SeqNr, + Payload = p + }.ToByteString(); + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/StreamRefs.cs b/src/core/Akka.Streams/StreamRefs.cs new file mode 100644 index 00000000000..422647d1aec --- /dev/null +++ b/src/core/Akka.Streams/StreamRefs.cs @@ -0,0 +1,114 @@ +#region copyright +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- +#endregion + +using System; +using Akka.Actor; +using Akka.Pattern; +using Akka.Streams.Dsl; +using Akka.Streams.Implementation; + +namespace Akka.Streams +{ + /// + /// A allows sharing a "reference" to a with others, + /// with the main purpose of crossing a network boundary. Usually obtaining a SinkRef would be done via Actor messaging, + /// in which one system asks a remote one, to accept some data from it, and the remote one decides to accept the + /// request to send data in a back-pressured streaming fashion -- using a sink ref. + /// + /// To create a you have to materialize the that you want to obtain + /// a reference to by attaching it to a . + /// + /// Stream refs can be seen as Reactive Streams over network boundaries. + /// + /// For additional configuration see `reference.conf` as well as . + /// + /// + public interface ISinkRef + { + Sink Sink { get; } + } + + /// + /// A SourceRef allows sharing a "reference" with others, with the main purpose of crossing a network boundary. + /// Usually obtaining a SourceRef would be done via Actor messaging, in which one system asks a remote one, + /// to share some data with it, and the remote one decides to do so in a back-pressured streaming fashion -- using a stream ref. + /// + /// To create a you have to materialize the that you want to + /// obtain a reference to by attaching it to a . + /// + /// Stream refs can be seen as Reactive Streams over network boundaries. + /// + /// For additional configuration see `reference.conf` as well as . + /// + /// + public interface ISourceRef + { + Source Source { get; } + } + + public sealed class TargetRefNotInitializedYetException : IllegalStateException + { + public TargetRefNotInitializedYetException() : base( + "Internal remote target actor ref not yet resolved, yet attempted to send messages to it. " + + "This should not happen due to proper flow-control, please open a ticket on the issue tracker: https://github.com/akkadotnet/akka.net") + { + } + } + + public sealed class StreamRefSubscriptionTimeoutException : IllegalStateException + { + public StreamRefSubscriptionTimeoutException(string message) : base(message) + { + } + } + + public sealed class RemoteStreamRefActorTerminatedException : Exception + { + public RemoteStreamRefActorTerminatedException(string message) : base(message) + { + } + } + + public sealed class InvalidSequenceNumberException : IllegalStateException + { + public long ExpectedSeqNr { get; } + public long GotSeqNr { get; } + + public InvalidSequenceNumberException(long expectedSeqNr, long gotSeqNr, string message) : base( + $"{message} (expected: {expectedSeqNr}, got: {gotSeqNr}). In most cases this means that message loss on this connection has occurred and the stream will fail eagerly.") + { + ExpectedSeqNr = expectedSeqNr; + GotSeqNr = gotSeqNr; + } + } + + /// + /// Stream refs establish a connection between a local and remote actor, representing the origin and remote sides + /// of a stream. Each such actor refers to the other side as its "partner". We make sure that no other actor than + /// the initial partner can send demand/messages to the other side accidentally. + /// + /// This exception is thrown when a message is recived from a non-partner actor, + /// which could mean a bug or some actively malicient behavior from the other side. + /// + /// This is not meant as a security feature, but rather as plain sanity-check. + /// + public sealed class InvalidPartnerActorException : IllegalStateException + { + public IActorRef ExpectedRef { get; } + public IActorRef GotRef { get; } + + public InvalidPartnerActorException(IActorRef expectedRef, IActorRef gotRef, string message) : base( + $"{message} (expected: {expectedRef}, got: {gotRef}). " + + "This may happen due to 'double-materialization' on the other side of this stream ref. " + + "Do note that stream refs are one-shot references and have to be paired up in 1:1 pairs. " + + "Multi-cast such as broadcast etc can be implemented by sharing multiple new stream references. ") + { + } + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/reference.conf b/src/core/Akka.Streams/reference.conf index f6815aa6b88..26b8f553e52 100644 --- a/src/core/Akka.Streams/reference.conf +++ b/src/core/Akka.Streams/reference.conf @@ -83,6 +83,30 @@ akka { # Note: If you change this value also change the fallback value in ActorMaterializerSettings fuzzing-mode = off } + + stream-ref { + # Buffer of a SinkRef that is used to batch Request elements from the other side of the stream ref + # + # The buffer will be attempted to be filled eagerly even while the local stage did not request elements, + # because the delay of requesting over network boundaries is much higher. + buffer-capacity = 32 + + # Demand is signalled by sending a cumulative demand message ("requesting messages until the n-th sequence number) + # Using a cumulative demand model allows us to re-deliver the demand message in case of message loss (which should + # be very rare in any case, yet possible -- mostly under connection break-down and re-establishment). + # + # The semantics of handling and updating the demand however are in-line with what Reactive Streams dictates. + # + # In normal operation, demand is signalled in response to arriving elements, however if no new elements arrive + # within `demand-redelivery-interval` a re-delivery of the demand will be triggered, assuming that it may have gotten lost. + demand-redelivery-interval = 1 second + + # Subscription timeout, during which the "remote side" MUST subscribe (materialize) the handed out stream ref. + # This timeout does not have to be very low in normal situations, since the remote side may also need to + # prepare things before it is ready to materialize the reference. However the timeout is needed to avoid leaking + # in-active streams which are never subscribed to. + subscription-timeout = 30 seconds + } } # Fully qualified config path which holds the dispatcher configuration @@ -107,4 +131,20 @@ akka { ssl-config { protocol = "TLSv1" } + actor { + + serializers { + akka-stream-ref = "Akka.Streams.Serialization.StreamRefSerializer, Akka.Streams" + } + + serialization-bindings { + "Akka.Streams.Dsl.SinkRefImpl, Akka.Streams" = akka-stream-ref + "Akka.Streams.Dsl.SourceRefImpl, Akka.Streams" = akka-stream-ref + "Akka.Streams.Dsl.IStreamRefsProtocol, Akka.Streams" = akka-stream-ref + } + + serialization-identifiers { + "Akka.Streams.Serialization.StreamRefSerializer, Akka.Streams" = 30 + } + } } diff --git a/src/protobuf/StreamRefMessages.proto b/src/protobuf/StreamRefMessages.proto new file mode 100644 index 00000000000..69a5194a299 --- /dev/null +++ b/src/protobuf/StreamRefMessages.proto @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2009-2017 Lightbend Inc. + * Copyright (C) 2017-2018 Akka.NET project + */ + +syntax = 'proto3'; +package Akka.Streams.Serialization.Proto.Msg; +option optimize_for = SPEED; + +/************************************************* + StreamRefs (SourceRef / SinkRef) related formats +**************************************************/ + +message EventType { + string typeName = 1; +} + +message SinkRef { + ActorRef targetRef = 1; + EventType eventType = 2; +} + +message SourceRef { + ActorRef originRef = 1; + EventType eventType = 2; +} + +message ActorRef { + string path = 1; +} + +message Payload { + bytes enclosedMessage = 1; + int32 serializerId = 2; + bytes messageManifest = 3; +} + +// stream refs protocol + +message OnSubscribeHandshake { + ActorRef targetRef = 1; +} +message CumulativeDemand { + int64 seqNr = 1; +} + +message SequencedOnNext { + int64 seqNr = 1; + Payload payload = 2; +} + +message RemoteStreamFailure { + bytes cause = 1; +} + +message RemoteStreamCompleted { + int64 seqNr = 1; +} \ No newline at end of file From adf4d41641ba8aaba103f83719ebdc15cbc66fa9 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Sun, 8 Jul 2018 11:05:23 +0200 Subject: [PATCH 11/14] Flow Ask and Watch operations --- src/Akka.sln | 1 + .../CoreAPISpec.ApproveCore.approved.txt | 7 +- .../CoreAPISpec.ApproveStreams.approved.txt | 113 +++++- .../Akka.Streams.Tests/Dsl/FlowAskSpec.cs | 347 ++++++++++++++++++ src/core/Akka.Streams/Dsl/Flow.cs | 45 +++ src/core/Akka.Streams/Dsl/FlowOperations.cs | 50 ++- .../Dsl/Internal/InternalFlowOperations.cs | 49 ++- src/core/Akka.Streams/Dsl/Source.cs | 44 +++ src/core/Akka.Streams/Dsl/SourceOperations.cs | 50 ++- .../Implementation/Fusing/Watch.cs | 54 +++ .../Implementation/Stages/Stages.cs | 1 + .../WatchedActorTerminatedException.cs | 41 +++ .../Akka.Tests.Shared.Internals/AkkaSpec.cs | 4 +- 13 files changed, 731 insertions(+), 75 deletions(-) create mode 100644 src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs create mode 100644 src/core/Akka.Streams/Implementation/Fusing/Watch.cs create mode 100644 src/core/Akka.Streams/WatchedActorTerminatedException.cs diff --git a/src/Akka.sln b/src/Akka.sln index 98ca938452f..c55d33bc479 100644 --- a/src/Akka.sln +++ b/src/Akka.sln @@ -83,6 +83,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A59BAE84-70E2-46A0-9E26-7413C103E2D7}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + ..\.gitignore = ..\.gitignore Akka.sln.DotSettings = Akka.sln.DotSettings NuGet.Config = NuGet.Config EndProjectSection diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt index c361fbbdba9..2df50ec9594 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveCore.approved.txt @@ -113,6 +113,7 @@ namespace Akka.Actor protected void Stash(Akka.Dispatch.SysMsg.SystemMessage msg) { } public void Stop(Akka.Actor.IActorRef child) { } public void Stop() { } + protected void StopFunctionRefs() { } public void Suspend() { } protected void TellWatchersWeDied() { } public void TerminatedQueuedFor(Akka.Actor.IActorRef subject) { } @@ -1702,12 +1703,15 @@ namespace Akka.Actor protected override void PreRestart(System.Exception reason, object message) { } protected override bool Receive(object message) { } } - public sealed class Terminated : Akka.Actor.IAutoReceivedMessage, Akka.Actor.INoSerializationVerificationNeeded, Akka.Actor.IPossiblyHarmful, Akka.Event.IDeadLetterSuppression + public sealed class Terminated : Akka.Actor.IAutoReceivedMessage, Akka.Actor.INoSerializationVerificationNeeded, Akka.Actor.IPossiblyHarmful, Akka.Event.IDeadLetterSuppression, System.IEquatable { public Terminated(Akka.Actor.IActorRef actorRef, bool existenceConfirmed, bool addressTerminated) { } public Akka.Actor.IActorRef ActorRef { get; } public bool AddressTerminated { get; } public bool ExistenceConfirmed { get; } + public bool Equals(Akka.Actor.Terminated other) { } + public override bool Equals(object obj) { } + public override int GetHashCode() { } public override string ToString() { } } public class TerminatedProps : Akka.Actor.Props @@ -4575,6 +4579,7 @@ namespace Akka.Util { public const string Base64Chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+~"; public static string Base64Encode(this long value) { } + public static System.Text.StringBuilder Base64Encode(this long value, System.Text.StringBuilder sb) { } public static string Base64Encode(this string s) { } } public class static BitArrayHelpers diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index 036a2baf588..b3bbf00111b 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -72,10 +72,11 @@ namespace Akka.Streams public readonly int MaxFixedBufferSize; public readonly int MaxInputBufferSize; public readonly int OutputBurstLimit; + public readonly Akka.Streams.Dsl.StreamRefSettings StreamRefSettings; public readonly Akka.Streams.StreamSubscriptionTimeoutSettings SubscriptionTimeoutSettings; public readonly Akka.Streams.Supervision.Decider SupervisionDecider; public readonly int SyncProcessingLimit; - public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Akka.Streams.Supervision.Decider supervisionDecider, Akka.Streams.StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = 1000) { } + public ActorMaterializerSettings(int initialInputBufferSize, int maxInputBufferSize, string dispatcher, Akka.Streams.Supervision.Decider supervisionDecider, Akka.Streams.StreamSubscriptionTimeoutSettings subscriptionTimeoutSettings, Akka.Streams.Dsl.StreamRefSettings streamRefSettings, bool isDebugLogging, int outputBurstLimit, bool isFuzzingMode, bool isAutoFusing, int maxFixedBufferSize, int syncProcessingLimit = 1000) { } public static Akka.Streams.ActorMaterializerSettings Create(Akka.Actor.ActorSystem system) { } public Akka.Streams.ActorMaterializerSettings WithAutoFusing(bool isAutoFusing) { } public Akka.Streams.ActorMaterializerSettings WithDebugLogging(bool isEnabled) { } @@ -83,6 +84,7 @@ namespace Akka.Streams public Akka.Streams.ActorMaterializerSettings WithFuzzingMode(bool isFuzzingMode) { } public Akka.Streams.ActorMaterializerSettings WithInputBuffer(int initialSize, int maxSize) { } public Akka.Streams.ActorMaterializerSettings WithMaxFixedBufferSize(int maxFixedBufferSize) { } + public Akka.Streams.ActorMaterializerSettings WithStreamRefSettings(Akka.Streams.Dsl.StreamRefSettings settings) { } public Akka.Streams.ActorMaterializerSettings WithSubscriptionTimeoutSettings(Akka.Streams.StreamSubscriptionTimeoutSettings settings) { } public Akka.Streams.ActorMaterializerSettings WithSupervisionStrategy(Akka.Streams.Supervision.Decider decider) { } public Akka.Streams.ActorMaterializerSettings WithSyncProcessingLimit(int limit) { } @@ -605,11 +607,27 @@ namespace Akka.Streams { protected InPort() { } } + public sealed class InvalidPartnerActorException : Akka.Pattern.IllegalStateException + { + public InvalidPartnerActorException(Akka.Actor.IActorRef expectedRef, Akka.Actor.IActorRef gotRef, string message) { } + public Akka.Actor.IActorRef ExpectedRef { get; } + public Akka.Actor.IActorRef GotRef { get; } + } + public sealed class InvalidSequenceNumberException : Akka.Pattern.IllegalStateException + { + public InvalidSequenceNumberException(long expectedSeqNr, long gotSeqNr, string message) { } + public long ExpectedSeqNr { get; } + public long GotSeqNr { get; } + } public interface IQueueOfferResult { } public interface ISinkQueue { System.Threading.Tasks.Task> PullAsync(); } + public interface ISinkRef + { + Akka.Streams.Dsl.Sink Sink { get; } + } public interface ISourceQueue { System.Threading.Tasks.Task OfferAsync(T element); @@ -621,6 +639,10 @@ namespace Akka.Streams void Fail(System.Exception ex); new System.Threading.Tasks.Task WatchCompletionAsync(); } + public interface ISourceRef + { + Akka.Streams.Dsl.Source Source { get; } + } public interface ITransformerLike { bool IsComplete { get; } @@ -719,6 +741,10 @@ namespace Akka.Streams public static readonly Akka.Streams.QueueOfferResult.QueueClosed Instance; } } + public sealed class RemoteStreamRefActorTerminatedException : System.Exception + { + public RemoteStreamRefActorTerminatedException(string message) { } + } public abstract class Shape : System.ICloneable { protected Shape() { } @@ -765,6 +791,20 @@ namespace Akka.Streams public StreamLimitReachedException(long max) { } protected StreamLimitReachedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } } + public class static StreamRefAttributes + { + public static Akka.Streams.Attributes CreateSubscriptionTimeout(System.TimeSpan timeout) { } + public interface IStreamRefAttribute : Akka.Streams.Attributes.IAttribute { } + public sealed class SubscriptionTimeout : Akka.Streams.Attributes.IAttribute, Akka.Streams.StreamRefAttributes.IStreamRefAttribute + { + public SubscriptionTimeout(System.TimeSpan timeout) { } + public System.TimeSpan Timeout { get; } + } + } + public sealed class StreamRefSubscriptionTimeoutException : Akka.Pattern.IllegalStateException + { + public StreamRefSubscriptionTimeoutException(string message) { } + } public sealed class StreamSubscriptionTimeoutSettings : System.IEquatable { public readonly Akka.Streams.StreamSubscriptionTimeoutTerminationMode Mode; @@ -793,6 +833,10 @@ namespace Akka.Streams Propagate = 0, Drain = 1, } + public sealed class TargetRefNotInitializedYetException : Akka.Pattern.IllegalStateException + { + public TargetRefNotInitializedYetException() { } + } public enum ThrottleMode { Shaping = 0, @@ -834,6 +878,11 @@ namespace Akka.Streams public void Shutdown() { } public override string ToString() { } } + public class WatchedActorTerminatedException : Akka.Actor.AkkaException + { + public WatchedActorTerminatedException(string stageName, Akka.Actor.IActorRef actorRef) { } + protected WatchedActorTerminatedException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + } } namespace Akka.Streams.Actors { @@ -1103,6 +1152,7 @@ namespace Akka.Streams.Dsl public Akka.Streams.Implementation.IModule Module { get; } public Akka.Streams.FlowShape Shape { get; } public Akka.Streams.Dsl.Flow AddAttributes(Akka.Streams.Attributes attributes) { } + public Akka.Streams.Dsl.Flow Ask(Akka.Actor.IActorRef actorRef, System.TimeSpan timeout, int parallelism = 2) { } public Akka.Streams.Dsl.Flow Async() { } public Akka.Streams.Dsl.Flow ConcatMaterialized(Akka.Streams.IGraph, TMat2> that, System.Func materializedFunction) { } public Akka.Streams.Dsl.IRunnableGraph Join(Akka.Streams.IGraph, TMat2> flow) { } @@ -1199,6 +1249,7 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Flow Throttle(this Akka.Streams.Dsl.Flow flow, int cost, System.TimeSpan per, int maximumBurst, System.Func calculateCost, Akka.Streams.ThrottleMode mode) { } [System.ObsoleteAttribute("Use Via(GraphStage) instead. [1.1.2]")] public static Akka.Streams.Dsl.Flow Transform(this Akka.Streams.Dsl.Flow flow, System.Func> stageFactory) { } + public static Akka.Streams.Dsl.Flow Watch(this Akka.Streams.Dsl.Flow flow, Akka.Actor.IActorRef actorRef) { } public static Akka.Streams.Dsl.Flow WatchTermination(this Akka.Streams.Dsl.Flow flow, System.Func materializerFunction) where TIn : TOut { } public static Akka.Streams.Dsl.Flow Where(this Akka.Streams.Dsl.Flow flow, System.Predicate predicate) { } @@ -1652,6 +1703,7 @@ namespace Akka.Streams.Dsl public Akka.Streams.Implementation.IModule Module { get; } public Akka.Streams.SourceShape Shape { get; } public Akka.Streams.Dsl.Source AddAttributes(Akka.Streams.Attributes attributes) { } + public Akka.Streams.Dsl.Source Ask(Akka.Actor.IActorRef actorRef, System.TimeSpan timeout, int parallelism = 2) { } public Akka.Streams.Dsl.Source Async() { } public Akka.Streams.Dsl.Source Combine(Akka.Streams.Dsl.Source first, Akka.Streams.Dsl.Source second, System.Func, Akka.NotUsed>> strategy, params Akka.Streams.Dsl.Source<, >[] rest) { } public Akka.Streams.Dsl.Source ConcatMaterialized(Akka.Streams.IGraph, TMat2> that, System.Func materializedFunction) { } @@ -1752,6 +1804,7 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Source Throttle(this Akka.Streams.Dsl.Source flow, int cost, System.TimeSpan per, int maximumBurst, System.Func calculateCost, Akka.Streams.ThrottleMode mode) { } [System.ObsoleteAttribute("Use Via(GraphStage) instead. [1.1.2]")] public static Akka.Streams.Dsl.Source Transform(this Akka.Streams.Dsl.Source flow, System.Func> stageFactory) { } + public static Akka.Streams.Dsl.Source Watch(this Akka.Streams.Dsl.Source flow, Akka.Actor.IActorRef actorRef) { } public static Akka.Streams.Dsl.Source WatchTermination(this Akka.Streams.Dsl.Source flow, System.Func materializerFunction) { } public static Akka.Streams.Dsl.Source Where(this Akka.Streams.Dsl.Source flow, System.Predicate predicate) { } public static Akka.Streams.Dsl.Source WhereNot(this Akka.Streams.Dsl.Source flow, System.Predicate predicate) { } @@ -1766,6 +1819,27 @@ namespace Akka.Streams.Dsl public static Akka.Streams.Dsl.Source> FromInputStream(System.Func createInputStream, int chunkSize = 8192) { } public static Akka.Streams.Dsl.Sink> FromOutputStream(System.Func createOutputStream, bool autoFlush = False) { } } + [Akka.Annotations.ApiMayChangeAttribute()] + public class static StreamRefs + { + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Source>> SinkRef() { } + [Akka.Annotations.ApiMayChangeAttribute()] + public static Akka.Streams.Dsl.Sink>> SourceRef() { } + } + public sealed class StreamRefSettings + { + public StreamRefSettings(int bufferCapacity, System.TimeSpan demandRedeliveryInterval, System.TimeSpan subscriptionTimeout) { } + public int BufferCapacity { get; } + public System.TimeSpan DemandRedeliveryInterval { get; } + public string ProductPrefix { get; } + public System.TimeSpan SubscriptionTimeout { get; } + public Akka.Streams.Dsl.StreamRefSettings Copy(System.Nullable bufferCapacity = null, System.Nullable demandRedeliveryInterval = null, System.Nullable subscriptionTimeout = null) { } + public static Akka.Streams.Dsl.StreamRefSettings Create(Akka.Configuration.Config config) { } + public Akka.Streams.Dsl.StreamRefSettings WithBufferCapacity(int value) { } + public Akka.Streams.Dsl.StreamRefSettings WithDemandRedeliveryInterval(System.TimeSpan value) { } + public Akka.Streams.Dsl.StreamRefSettings WithSubscriptionTimeout(System.TimeSpan value) { } + } public abstract class SubFlow : Akka.Streams.Dsl.IFlow { protected SubFlow() { } @@ -4067,6 +4141,7 @@ namespace Akka.Streams.Implementation.Stages public static readonly Akka.Streams.Attributes UnfoldResourceSource; public static readonly Akka.Streams.Attributes UnfoldResourceSourceAsync; public static readonly Akka.Streams.Attributes Unzip; + public static readonly Akka.Streams.Attributes Watch; public static readonly Akka.Streams.Attributes Where; public static readonly Akka.Streams.Attributes Zip; public static readonly Akka.Streams.Attributes ZipN; @@ -4116,6 +4191,16 @@ namespace Akka.Streams.IO public static Akka.Streams.IO.IOResult Success(long count) { } } } +namespace Akka.Streams.Serialization +{ + public sealed class StreamRefSerializer : Akka.Serialization.SerializerWithStringManifest + { + public StreamRefSerializer(Akka.Actor.ExtendedActorSystem system) { } + public override object FromBinary(byte[] bytes, string manifest) { } + public override string Manifest(object o) { } + public override byte[] ToBinary(object o) { } + } +} namespace Akka.Streams.Stage { [System.ObsoleteAttribute("Please use GraphStage instead. [1.1.2]")] @@ -4209,7 +4294,9 @@ namespace Akka.Streams.Stage public Akka.Event.ILoggingAdapter Log { get; } protected object LogSource { get; } protected Akka.Streams.IMaterializer Materializer { get; } - public Akka.Streams.Stage.StageActorRef StageActorRef { get; } + public Akka.Streams.Stage.StageActor StageActor { get; } + [Akka.Annotations.ApiMayChangeAttribute()] + protected virtual string StageActorName { get; } protected Akka.Streams.IMaterializer SubFusingMaterializer { get; } protected internal void AbortEmitting(Akka.Streams.Outlet outlet) { } protected void AbortReading(Akka.Streams.Inlet inlet) { } @@ -4234,7 +4321,7 @@ namespace Akka.Streams.Stage protected Akka.Streams.Stage.IInHandler GetHandler(Akka.Streams.Inlet inlet) { } protected Akka.Streams.Stage.IOutHandler GetHandler(Akka.Streams.Outlet outlet) { } [Akka.Annotations.ApiMayChangeAttribute()] - protected Akka.Streams.Stage.StageActorRef GetStageActorRef(Akka.Streams.Stage.StageActorRef.Receive receive) { } + protected Akka.Streams.Stage.StageActor GetStageActor(Akka.Streams.Stage.StageActorRef.Receive receive) { } protected internal T Grab(Akka.Streams.Inlet inlet) { } protected bool HasBeenPulled(Akka.Streams.Inlet inlet) { } protected internal bool IsAvailable(Akka.Streams.Inlet inlet) { } @@ -4482,21 +4569,17 @@ namespace Akka.Streams.Stage protected PushStage() { } public virtual Akka.Streams.Stage.ISyncDirective OnPull(Akka.Streams.Stage.IContext context) { } } - public sealed class StageActorRef : Akka.Actor.MinimalActorRef + public sealed class StageActor { - public readonly Akka.Event.ILoggingAdapter Log; - public static readonly Akka.Streams.Implementation.EnumerableActorName Name; - public readonly System.Collections.Immutable.IImmutableSet StageTerminatedTombstone; - public StageActorRef(Akka.Actor.IActorRefProvider provider, Akka.Event.ILoggingAdapter log, System.Func>> getAsyncCallback, Akka.Streams.Stage.StageActorRef.Receive initialReceive, Akka.Actor.ActorPath path) { } - public override bool IsTerminated { get; } - public override Akka.Actor.ActorPath Path { get; } - public override Akka.Actor.IActorRefProvider Provider { get; } - public void Become(Akka.Streams.Stage.StageActorRef.Receive behavior) { } - public override void SendSystemMessage(Akka.Dispatch.SysMsg.ISystemMessage message) { } - public override void Stop() { } - protected override void TellInternal(object message, Akka.Actor.IActorRef sender) { } + public StageActor(Akka.Streams.ActorMaterializer materializer, System.Func>> getAsyncCallback, Akka.Streams.Stage.StageActorRef.Receive initialReceive, string name = null) { } + public Akka.Actor.IActorRef Ref { get; } + public void Become(Akka.Streams.Stage.StageActorRef.Receive receive) { } + public void Stop() { } public void Unwatch(Akka.Actor.IActorRef actorRef) { } public void Watch(Akka.Actor.IActorRef actorRef) { } + } + public class static StageActorRef + { public delegate void Receive(System.Tuple args); } public class StageActorRefNotInitializedException : System.Exception diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs new file mode 100644 index 00000000000..46a30f63092 --- /dev/null +++ b/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs @@ -0,0 +1,347 @@ +#region copyright +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- +#endregion + +using System; +using System.Linq; +using System.Threading.Tasks; +using Akka.Actor; +using Akka.Streams.Dsl; +using Akka.Streams.TestKit; +using Akka.Streams.TestKit.Tests; +using Akka.TestKit; +using Akka.TestKit.TestActors; +using Akka.Util; +using FluentAssertions; +using Xunit; +using Xunit.Abstractions; + +namespace Akka.Streams.Tests.Dsl +{ + public class FlowAskSpec : AkkaSpec + { + #region internal classes + + sealed class Reply : IEquatable + { + public int Payload { get; } + + public Reply(int payload) + { + Payload = payload; + } + + public bool Equals(Reply other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Payload == other.Payload; + } + + public override bool Equals(object obj) => obj is Reply reply && Equals(reply); + public override int GetHashCode() => Payload; + } + + sealed class Replier : UntypedActor + { + protected override void OnReceive(object message) => Sender.Tell(new Reply((int)message)); + } + + sealed class ReplyAndProxy : ReceiveActor + { + public ReplyAndProxy(IActorRef to) + { + Receive(msg => + { + to.Tell(msg); + Sender.Tell(new Reply(msg)); + }); + } + } + + sealed class RandomDelaysReplier : ReceiveActor + { + public RandomDelaysReplier() + { + Receive(msg => + { + var replyTo = Sender; + Task.Run(async () => + { + await Task.Delay(ThreadLocalRandom.Current.Next(1, 10)); + replyTo.Tell(new Reply(msg)); + }); + }); + } + } + + sealed class StatusReplier : UntypedActor + { + protected override void OnReceive(object message) => + Sender.Tell(new Status.Success(new Reply((int)message))); + } + + sealed class FailOn : ReceiveActor + { + public FailOn(int n) + { + Receive(msg => + { + if (msg == n) + Sender.Tell(new Status.Failure(new Exception($"Booming for {n}!"))); + else + Sender.Tell(new Status.Success(new Reply(msg))); + }); + } + } + + sealed class FailOnAllExcept : ReceiveActor + { + public FailOnAllExcept(int n) + { + Receive(msg => + { + if (msg == n) + Sender.Tell(new Status.Success(new Reply(msg))); + else + Sender.Tell(new Status.Failure(new Exception($"Booming for {n}!"))); + }); + } + } + + #endregion + + private readonly IMaterializer _materializer; + private readonly TimeSpan _timeout = 10.Seconds(); + + public FlowAskSpec(ITestOutputHelper output) : base(output) + { + _materializer = Sys.Materializer(); + } + + [Fact] + public void Flow_with_ask_must_produce_asked_elements() => this.AssertAllStagesStopped(() => + { + var replyOnInts = + Sys.ActorOf(Props.Create(() => new Replier()), + "replyOnInts"); + var c = this.CreateManualSubscriberProbe(); + + var p = Source.From(Enumerable.Range(1, 3)) + .Ask(replyOnInts, _timeout, 4) + .RunWith(Sink.FromSubscriber(c), _materializer); + + var sub = c.ExpectSubscription(); + sub.Request(2); + c.ExpectNext(new Reply(1)); + c.ExpectNext(new Reply(2)); + c.ExpectNoMsg(200.Milliseconds()); + sub.Request(2); + c.ExpectNext(new Reply(3)); + c.ExpectComplete(); + }, _materializer); + + [Fact] + public void Flow_with_ask_must_produce_asked_elements_for_simple_ask() => this.AssertAllStagesStopped(() => + { + var replyOnInts = Sys.ActorOf(Props.Create(() => new Replier()), "replyOnInts"); + var c = this.CreateManualSubscriberProbe(); + + var p = Source.From(Enumerable.Range(1, 3)) + .Ask(replyOnInts, _timeout) + .RunWith(Sink.FromSubscriber(c), _materializer); + + var sub = c.ExpectSubscription(); + sub.Request(2); + c.ExpectNext(new Reply(1)); + c.ExpectNext(new Reply(2)); + c.ExpectNoMsg(200.Milliseconds()); + sub.Request(2); + c.ExpectNext(new Reply(3)); + c.ExpectComplete(); + }, _materializer); + + [Fact] + public void Flow_with_ask_must_produce_asked_elements_when_response_is_Status_Success() => this.AssertAllStagesStopped(() => + { + var statusReplier = Sys.ActorOf(Props.Create(() => new StatusReplier()), "statusReplier"); + var c = this.CreateManualSubscriberProbe(); + + var p = Source.From(Enumerable.Range(1, 3)) + .Ask(statusReplier, _timeout, 4) + .RunWith(Sink.FromSubscriber(c), _materializer); + + var sub = c.ExpectSubscription(); + sub.Request(2); + c.ExpectNext(new Reply(1)); + c.ExpectNext(new Reply(2)); + c.ExpectNoMsg(200.Milliseconds()); + sub.Request(2); + c.ExpectNext(new Reply(3)); + c.ExpectComplete(); + }, _materializer); + + [Fact] + public void Flow_with_ask_must_produce_future_elements_in_order() + { + var replyRandomDelays = Sys.ActorOf(Props.Create(() => new RandomDelaysReplier()), "replyRandomDelays"); + var c = this.CreateManualSubscriberProbe(); + + var p = Source.From(Enumerable.Range(1, 50)) + .Ask(replyRandomDelays, _timeout, 4) + .RunWith(Sink.FromSubscriber(c), _materializer); + + var sub = c.ExpectSubscription(); + sub.Request(1000); + for (int i = 1; i <= 50; i++) + c.ExpectNext(new Reply(i)); + + c.ExpectComplete(); + } + + [Fact] + public void Flow_with_ask_must_signal_ask_timeout_failure() => this.AssertAllStagesStopped(() => + { + var dontReply = Sys.ActorOf(BlackHoleActor.Props, "dontReply"); + var c = this.CreateManualSubscriberProbe(); + + var p = Source.From(Enumerable.Range(1, 50)) + .Ask(dontReply, 10.Milliseconds(), 4) + .RunWith(Sink.FromSubscriber(c), _materializer); + + c.ExpectSubscription().Request(10); + var error = c.ExpectError(); + error.As().Flatten() + .InnerException + .Should().BeOfType(); + }, _materializer); + + [Fact] + public void Flow_with_ask_must_signal_ask_failure() => this.AssertAllStagesStopped(() => + { + var failsOn = ReplierFailOn(1); + var c = this.CreateManualSubscriberProbe(); + + var p = Source.From(Enumerable.Range(1, 5)) + .Ask(failsOn, _timeout, 4) + .RunWith(Sink.FromSubscriber(c), _materializer); + + c.ExpectSubscription().Request(10); + var error = c.ExpectError().As(); + error.Flatten().InnerException.Message.Should().Be("Booming for 1!"); + }, _materializer); + + [Fact] + public void Flow_with_ask_signal_failure_when_target_actor_is_terminated() => this.AssertAllStagesStopped(() => + { + var r = Sys.ActorOf(Props.Create(() => new Replier()), "replyRandomDelays"); + var done = Source.Maybe() + .Ask(r, _timeout, 4) + .RunWith(Sink.Ignore(), _materializer); + + Intercept(() => + { + r.Tell(PoisonPill.Instance); + done.Wait(RemainingOrDefault); + }) + .Flatten() + .InnerException.Should().BeOfType(); + + }, _materializer); + + [Fact] + public void Flow_with_ask_a_failure_mid_stream_must_skip_element_with_resume_strategy() => this.AssertAllStagesStopped(() => + { + var p = CreateTestProbe(); + var input = new[] { "a", "b", "c", "d", "e", "f" }; + var elements = Source.From(input) + .Ask(p.Ref, _timeout, 5) + .WithAttributes(ActorAttributes.CreateSupervisionStrategy(Supervision.Deciders.ResumingDecider)) + .RunWith(Sink.Seq(), _materializer); + + // the problematic ordering: + p.ExpectMsg("a"); + p.LastSender.Tell("a"); + + p.ExpectMsg("b"); + p.LastSender.Tell("b"); + + p.ExpectMsg("c"); + var cSender = p.LastSender; + + p.ExpectMsg("d"); + p.LastSender.Tell("d"); + + p.ExpectMsg("e"); + p.LastSender.Tell("e"); + + p.ExpectMsg("f"); + p.LastSender.Tell("f"); + + cSender.Tell(new Status.Failure(new Exception("Boom!"))); + elements.Result.Should().BeEquivalentTo(new[] { "a", "b", /*no c*/ "d", "e", "f" }); + + }, _materializer); + + [Fact] + public void Flow_with_ask_must_resume_after_ask_failure() => this.AssertAllStagesStopped(() => + { + var c = this.CreateManualSubscriberProbe(); + var aref = ReplierFailOn(3); + var p = Source.From(Enumerable.Range(1, 5)) + .Ask(aref, _timeout, 4) + .WithAttributes(ActorAttributes.CreateSupervisionStrategy(Supervision.Deciders.ResumingDecider)) + .To(Sink.FromSubscriber(c)) + .Run(_materializer); + + var sub = c.ExpectSubscription(); + sub.Request(10); + foreach (var i in new[] { 1, 2, 4, 5 }) + { + c.ExpectNext(new Reply(i)); + } + + c.ExpectComplete(); + + }, _materializer); + + [Fact] + public void Flow_with_ask_must_resume_after_multiple_failures() => this.AssertAllStagesStopped(() => + { + var aref = ReplierFailAllExceptOn(6); + var t = Source.From(Enumerable.Range(1, 6)) + .Ask(aref, _timeout, 2) + .WithAttributes(ActorAttributes.CreateSupervisionStrategy(Supervision.Deciders.ResumingDecider)) + .RunWith(Sink.First(), _materializer); + + t.Wait(3.Seconds()).Should().BeTrue(); + t.Result.Should().Be(new Reply(6)); + }, _materializer); + + [Fact] + public void Flow_with_ask_should_handle_cancel_properly() => this.AssertAllStagesStopped(() => + { + var dontReply = Sys.ActorOf(BlackHoleActor.Props, "dontReply"); + var pub = this.CreateManualPublisherProbe(); + var sub = this.CreateManualSubscriberProbe(); + + var p = Source.FromPublisher(pub) + .Ask(dontReply, _timeout, 4) + .RunWith(Sink.FromSubscriber(sub), _materializer); + + var upstream = pub.ExpectSubscription(); + upstream.ExpectRequest(); + sub.ExpectSubscription().Cancel(); + upstream.ExpectCancellation(); + }, _materializer); + + private IActorRef ReplierFailOn(int n) => Sys.ActorOf(Props.Create(() => new FailOn(n)), "failureReplier-" + n); + + private IActorRef ReplierFailAllExceptOn(int n) => Sys.ActorOf(Props.Create(() => new FailOnAllExcept(n)), "failureReplier-" + n); + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/Dsl/Flow.cs b/src/core/Akka.Streams/Dsl/Flow.cs index a51bf68f803..dafbe66a24e 100644 --- a/src/core/Akka.Streams/Dsl/Flow.cs +++ b/src/core/Akka.Streams/Dsl/Flow.cs @@ -8,6 +8,8 @@ using System; using System.Collections.Immutable; using System.Linq; +using System.Runtime.ExceptionServices; +using Akka.Actor; using Akka.Streams.Dsl.Internal; using Akka.Streams.Implementation; using Akka.Streams.Implementation.Fusing; @@ -181,6 +183,49 @@ public Flow AddAttributes(Attributes attributes) /// TBD public Flow Async() => AddAttributes(new Attributes(Attributes.AsyncBoundary.Instance)); + /// + /// Use the `ask` pattern to send a request-reply message to the target . + /// If any of the asks times out it will fail the stream with a . + /// + /// Parallelism limits the number of how many asks can be "in flight" at the same time. + /// Please note that the elements emitted by this operator are in-order with regards to the asks being issued + /// (i.e. same behaviour as ). + /// + /// The operator fails with an if the target actor is terminated, + /// or with an in case the ask exceeds the timeout passed in. + /// + /// Adheres to the attribute. + /// + /// '''Emits when''' the futures (in submission order) created by the ask pattern internally are completed. + /// '''Backpressures when''' the number of futures reaches the configured parallelism and the downstream backpressures. + /// '''Completes when''' upstream completes and all futures have been completed and all elements have been emitted. + /// '''Fails when''' the passed in actor terminates, or a timeout is exceeded in any of the asks performed. + /// '''Cancels when''' downstream cancels. + /// + public Flow Ask(IActorRef actorRef, TimeSpan timeout, int parallelism = 2) + { + // I know this is not a place for it, but since Ask generic param must be supplied, it's better + // if it remain alone in generic params list (no need to provide types that will be infered) + var askFlow = Flow.Create() + .Watch(actorRef) + .SelectAsync(parallelism, async e => { + var reply = await actorRef.Ask(e, timeout: timeout); + switch (reply) + { + case TOut2 a: return a; + case Status.Success s when s.Status is TOut2 a: return a; + case Status.Failure f: + ExceptionDispatchInfo.Capture(f.Cause).Throw(); + return default(TOut2); + default: + throw new InvalidOperationException($"Expected to receive response of type {nameof(TOut2)}, but got: {reply}"); + } + }) + .Named("ask"); + + return ViaMaterialized(askFlow, Keep.Left); + } + /// /// Transform the materialized value of this Flow, leaving all other properties as they were. /// diff --git a/src/core/Akka.Streams/Dsl/FlowOperations.cs b/src/core/Akka.Streams/Dsl/FlowOperations.cs index 52a1338b357..eccd46d3809 100644 --- a/src/core/Akka.Streams/Dsl/FlowOperations.cs +++ b/src/core/Akka.Streams/Dsl/FlowOperations.cs @@ -9,11 +9,11 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; +using Akka.Actor; using Akka.Event; using Akka.IO; using Akka.Streams.Dsl.Internal; using Akka.Streams.Stage; -using Akka.Streams.Supervision; using Akka.Streams.Util; // ReSharper disable UnusedMember.Global @@ -258,12 +258,12 @@ public static Flow StatefulSelectMany(this Fl /// are emitted downstream are in the same order as received from upstream. /// /// If the group by function throws an exception or if the is completed - /// with failure and the supervision decision is + /// with failure and the supervision decision is /// the stream will be completed with failure. /// /// If the group by function throws an exception or if the is completed - /// with failure and the supervision decision is or - /// the element is dropped and the stream continues. + /// with failure and the supervision decision is or + /// the element is dropped and the stream continues. /// /// Emits when the task returned by the provided function finishes for the next element in sequence /// @@ -301,12 +301,12 @@ public static Flow SelectAsync(this Flow throws an exception or if the is completed - /// with failure and the supervision decision is + /// with failure and the supervision decision is /// the stream will be completed with failure. /// /// If the group by function throws an exception or if the is completed - /// with failure and the supervision decision is or - /// the element is dropped and the stream continues. + /// with failure and the supervision decision is or + /// the element is dropped and the stream continues. /// /// Adheres to the attribute. /// @@ -606,7 +606,7 @@ public static Flow, TMat> Sliding(this F /// emitting the next current value. /// /// If the function throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Adheres to the attribute. @@ -640,11 +640,11 @@ public static Flow Scan(this Flow that resolves to the next current value. /// /// If the function throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// If the function throws an exception and the supervision decision is - /// current value starts at the previous + /// current value starts at the previous /// current value, or zero when it doesn't have one, and the stream will continue. /// /// @@ -677,7 +677,7 @@ public static Flow ScanAsync(this Flo /// yielding the next current value. /// /// If the function throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Adheres to the attribute. @@ -710,7 +710,7 @@ public static Flow Aggregate(this Flo /// yielding the next current value. /// /// If the function returns a failure and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Adheres to the attribute. @@ -1154,7 +1154,7 @@ public static Flow BatchWeighted(this /// This means that if the upstream is actually faster than the upstream it will be backpressured by the downstream /// subscriber. /// - /// Expand does not support and . + /// Expand does not support and . /// Exceptions from the function will complete the stream with failure. /// /// Emits when downstream stops backpressuring @@ -1260,11 +1260,11 @@ public static Flow, Source>, TMat /// to consume only one of them. /// /// If the group by function throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the group by throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Function MUST NOT return null. This will throw exception and trigger supervision decision mechanism. @@ -1330,11 +1330,11 @@ public static SubFlow> GroupBy throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the split throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Emits when an element for which the provided predicate is true, opening and emitting @@ -1401,11 +1401,11 @@ public static SubFlow> SplitWhen(th /// explicit buffers are filled. /// /// If the split throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the split throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Emits when an element passes through.When the provided predicate is true it emits the element @@ -2243,5 +2243,17 @@ public static Flow OrElse(this Flow flow, IGrap /// TBD public static Flow OrElseMaterialized(this Flow flow, IGraph, TMat2> secondary, Func materializedFunction) => (Flow)InternalFlowOperations.OrElseMaterialized(flow, secondary, materializedFunction); + + /// + /// The operator fails with an if the target actor is terminated. + /// + /// '''Emits when''' upstream emits + /// '''Backpressures when''' downstream backpressures + /// '''Completes when''' upstream completes + /// '''Fails when''' the watched actor terminates + /// '''Cancels when''' downstream cancels + /// + public static Flow Watch(this Flow flow, IActorRef actorRef) => + (Flow)InternalFlowOperations.Watch(flow, actorRef); } } diff --git a/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs b/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs index 97bb0210232..c84656ff49f 100644 --- a/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs +++ b/src/core/Akka.Streams/Dsl/Internal/InternalFlowOperations.cs @@ -10,12 +10,12 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; +using Akka.Actor; using Akka.Event; using Akka.IO; using Akka.Streams.Implementation; using Akka.Streams.Implementation.Stages; using Akka.Streams.Stage; -using Akka.Streams.Supervision; using Akka.Streams.Util; namespace Akka.Streams.Dsl.Internal @@ -274,12 +274,12 @@ public static IFlow StatefulSelectMany(this IFlow throws an exception or if the is completed - /// with failure and the supervision decision is + /// with failure and the supervision decision is /// the stream will be completed with failure. /// /// If the group by function throws an exception or if the is completed - /// with failure and the supervision decision is or - /// the element is dropped and the stream continues. + /// with failure and the supervision decision is or + /// the element is dropped and the stream continues. /// /// Emits when the task returned by the provided function finishes for the next element in sequence /// @@ -317,12 +317,12 @@ public static IFlow SelectAsync(this IFlow throws an exception or if the is completed - /// with failure and the supervision decision is + /// with failure and the supervision decision is /// the stream will be completed with failure. /// /// If the group by function throws an exception or if the is completed - /// with failure and the supervision decision is or - /// the element is dropped and the stream continues. + /// with failure and the supervision decision is or + /// the element is dropped and the stream continues. /// /// Emits when any of the tasks returned by the provided function complete /// @@ -602,7 +602,7 @@ public static IFlow, TMat> Sliding(this IFlow f /// emitting the next current value. /// /// If the function throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Emits when the function scanning the element returns a new element @@ -633,11 +633,11 @@ public static IFlow Scan(this IFlow flow /// emitting a that resolves to the next current value. /// /// If the function throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// If the function throws an exception and the supervision decision is - /// current value starts at the previous + /// current value starts at the previous /// current value, or zero when it doesn't have one, and the stream will continue. /// /// Emits the returned by completes @@ -667,7 +667,7 @@ public static IFlow ScanAsync(this IFlow /// yielding the next current value. /// /// If the function throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Emits when upstream completes @@ -697,7 +697,7 @@ public static IFlow Aggregate(this IFlow /// yielding the next current value. /// /// If the function returns a failure and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Emits when upstream completes @@ -1112,7 +1112,7 @@ public static IFlow BatchWeighted(this IFlow and . + /// Expand does not support and . /// Exceptions from the function will complete the stream with failure. /// /// Emits when downstream stops backpressuring @@ -1216,11 +1216,11 @@ public static IFlow, Source>, TMat> PrefixAn /// to consume only one of them. /// /// If the group by function throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the group by throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Function MUST NOT return null. This will throw exception and trigger supervision decision mechanism. @@ -1338,11 +1338,11 @@ public IFlow Apply(Flow flow, int breadth) /// explicit buffers are filled. /// /// If the split throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the split throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Emits when an element for which the provided predicate is true, opening and emitting @@ -1443,11 +1443,11 @@ public IFlow Apply(Flow flow, int breadth) /// explicit buffers are filled. /// /// If the split throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the split throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Emits when an element passes through.When the provided predicate is true it emitts the element @@ -2445,6 +2445,17 @@ public static IFlow Monitor(this IFlow flow, return flow.ViaMaterialized(new Fusing.MonitorFlow(), combine); } + /// + /// The operator fails with an if the target actor is terminated. + /// + /// '''Emits when''' upstream emits + /// '''Backpressures when''' downstream backpressures + /// '''Completes when''' upstream completes + /// '''Fails when''' the watched actor terminates + /// '''Cancels when''' downstream cancels + /// + public static IFlow Watch(this IFlow flow, IActorRef actorRef) => flow.Via(new Fusing.Watch(actorRef)); + //TODO: there is no HKT in .NET, so we cannot simply do `to` method, which evaluates to either Source ⇒ IRunnableGraph, or Flow ⇒ Sink } diff --git a/src/core/Akka.Streams/Dsl/Source.cs b/src/core/Akka.Streams/Dsl/Source.cs index e8e1f224137..cd78d11b284 100644 --- a/src/core/Akka.Streams/Dsl/Source.cs +++ b/src/core/Akka.Streams/Dsl/Source.cs @@ -10,6 +10,7 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection; +using System.Runtime.ExceptionServices; using System.Threading.Tasks; using Akka.Actor; using Akka.Streams.Dsl.Internal; @@ -160,6 +161,49 @@ public Source AddAttributes(Attributes attributes) /// TBD public Source Async() => AddAttributes(new Attributes(Attributes.AsyncBoundary.Instance)); + /// + /// Use the `ask` pattern to send a request-reply message to the target . + /// If any of the asks times out it will fail the stream with a . + /// + /// Parallelism limits the number of how many asks can be "in flight" at the same time. + /// Please note that the elements emitted by this operator are in-order with regards to the asks being issued + /// (i.e. same behaviour as ). + /// + /// The operator fails with an if the target actor is terminated, + /// or with an in case the ask exceeds the timeout passed in. + /// + /// Adheres to the attribute. + /// + /// '''Emits when''' the futures (in submission order) created by the ask pattern internally are completed. + /// '''Backpressures when''' the number of futures reaches the configured parallelism and the downstream backpressures. + /// '''Completes when''' upstream completes and all futures have been completed and all elements have been emitted. + /// '''Fails when''' the passed in actor terminates, or a timeout is exceeded in any of the asks performed. + /// '''Cancels when''' downstream cancels. + /// + public Source Ask(IActorRef actorRef, TimeSpan timeout, int parallelism = 2) + { + // I know this is not a place for it, but since Ask generic param must be supplied, it's better + // if it remain alone in generic params list (no need to provide types that will be infered) + var askFlow = Flow.Create() + .Watch(actorRef) + .SelectAsync(parallelism, async e => { + var reply = await actorRef.Ask(e, timeout: timeout); + switch (reply) + { + case TOut2 a: return a; + case Status.Success s when s.Status is TOut2 a: return a; + case Status.Failure f: + ExceptionDispatchInfo.Capture(f.Cause).Throw(); + return default(TOut2); + default: + throw new InvalidOperationException($"Expected to receive response of type {nameof(TOut2)}, but got: {reply}"); + } + }) + .Named("ask"); + + return ViaMaterialized(askFlow, Keep.Left); + } + /// /// Transform this by appending the given processing steps. /// The function is used to compose the materialized values of this flow and that diff --git a/src/core/Akka.Streams/Dsl/SourceOperations.cs b/src/core/Akka.Streams/Dsl/SourceOperations.cs index 3c8ee2e8de6..11b96bb0246 100644 --- a/src/core/Akka.Streams/Dsl/SourceOperations.cs +++ b/src/core/Akka.Streams/Dsl/SourceOperations.cs @@ -9,11 +9,11 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Threading.Tasks; +using Akka.Actor; using Akka.Event; using Akka.IO; using Akka.Streams.Dsl.Internal; using Akka.Streams.Stage; -using Akka.Streams.Supervision; using Akka.Streams.Util; // ReSharper disable UnusedMember.Global @@ -247,12 +247,12 @@ public static Source StatefulSelectMany(this So /// are emitted downstream are in the same order as received from upstream. /// /// If the group by function throws an exception or if the is completed - /// with failure and the supervision decision is + /// with failure and the supervision decision is /// the stream will be completed with failure. /// /// If the group by function throws an exception or if the is completed - /// with failure and the supervision decision is or - /// the element is dropped and the stream continues. + /// with failure and the supervision decision is or + /// the element is dropped and the stream continues. /// /// Emits when the task returned by the provided function finishes for the next element in sequence /// @@ -290,12 +290,12 @@ public static Source SelectAsync(this Source throws an exception or if the is completed - /// with failure and the supervision decision is + /// with failure and the supervision decision is /// the stream will be completed with failure. /// /// If the group by function throws an exception or if the is completed - /// with failure and the supervision decision is or - /// the element is dropped and the stream continues. + /// with failure and the supervision decision is or + /// the element is dropped and the stream continues. /// /// Emits when any of the tasks returned by the provided function complete /// @@ -573,7 +573,7 @@ public static Source, TMat> Sliding(this Source throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Emits when the function scanning the element returns a new element @@ -603,11 +603,11 @@ public static Source Scan(this Source that resolves to the next current value. /// /// If the function throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// If the function throws an exception and the supervision decision is - /// current value starts at the previous + /// current value starts at the previous /// current value, or zero when it doesn't have one, and the stream will continue. /// /// Emits the returned by completes @@ -636,7 +636,7 @@ public static Source ScanAsync(this Source throws an exception and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Emits when upstream completes @@ -665,7 +665,7 @@ public static Source Aggregate(this Source returns a failure and the supervision decision is - /// current value starts at again + /// current value starts at again /// the stream will continue. /// /// Emits when upstream completes @@ -1077,7 +1077,7 @@ public static Source BatchWeighted(this Source and . + /// Expand does not support and . /// Exceptions from the function will complete the stream with failure. /// /// Emits when downstream stops backpressuring @@ -1179,11 +1179,11 @@ public static Source, Source>, TMat> P /// to consume only one of them. /// /// If the group by function throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the group by throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Emits when an element for which the grouping function returns a group that has not yet been created. @@ -1242,11 +1242,11 @@ public static SubFlow> GroupBy throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the split throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Emits when an element for which the provided predicate is true, opening and emitting @@ -1310,11 +1310,11 @@ public static SubFlow> SplitWhen(th /// operator itself—and thereby all substreams—once all internal or explicit buffers are filled. /// /// If the split throws an exception and the supervision decision - /// is the stream and substreams will be completed + /// is the stream and substreams will be completed /// with failure. /// /// If the split throws an exception and the supervision decision - /// is or + /// is or /// the element is dropped and the stream and substreams continue. /// /// Emits when an element passes through.When the provided predicate is true it emits the element @@ -2140,5 +2140,17 @@ public static Source OrElse(this Source flow, IGraph< /// TBD public static Source OrElseMaterialized(this Source flow, IGraph, TMat2> secondary, Func materializedFunction) => (Source)InternalFlowOperations.OrElseMaterialized(flow, secondary, materializedFunction); + + /// + /// The operator fails with an if the target actor is terminated. + /// + /// '''Emits when''' upstream emits + /// '''Backpressures when''' downstream backpressures + /// '''Completes when''' upstream completes + /// '''Fails when''' the watched actor terminates + /// '''Cancels when''' downstream cancels + /// + public static Source Watch(this Source flow, IActorRef actorRef) => + (Source)InternalFlowOperations.Watch(flow, actorRef); } } diff --git a/src/core/Akka.Streams/Implementation/Fusing/Watch.cs b/src/core/Akka.Streams/Implementation/Fusing/Watch.cs new file mode 100644 index 00000000000..768d3768cf4 --- /dev/null +++ b/src/core/Akka.Streams/Implementation/Fusing/Watch.cs @@ -0,0 +1,54 @@ +#region copyright +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- +#endregion + +using System; +using Akka.Actor; +using Akka.Streams.Implementation.Stages; +using Akka.Streams.Stage; + +namespace Akka.Streams.Implementation.Fusing +{ + internal sealed class Watch : SimpleLinearGraphStage + { + #region logic + + private sealed class Logic : GraphStageLogic + { + private readonly Lazy _self; + private readonly IActorRef _targetRef; + + public Logic(Watch stage) : base(stage.Shape) + { + _targetRef = stage.ActorRef; + _self = new Lazy(() => GetStageActor(tuple => + { + if (tuple.Item2 is Terminated terminated && terminated.ActorRef.Equals(_targetRef)) + FailStage(new WatchedActorTerminatedException("watch", terminated.ActorRef)); + })); + + SetHandler(stage.Outlet, onPull: () => Pull(stage.Inlet)); + SetHandler(stage.Inlet, onPush: () => Push(stage.Outlet, Grab(stage.Inlet))); + } + + public override void PreStart() => _self.Value.Watch(_targetRef); + } + + #endregion + + public IActorRef ActorRef { get; } + + public Watch(IActorRef actorRef) + { + ActorRef = actorRef; + } + + protected override Attributes InitialAttributes { get; } = DefaultAttributes.Watch; + protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) => new Logic(this); + } +} \ No newline at end of file diff --git a/src/core/Akka.Streams/Implementation/Stages/Stages.cs b/src/core/Akka.Streams/Implementation/Stages/Stages.cs index 6b9fee3eed8..f78ad55ebb2 100644 --- a/src/core/Akka.Streams/Implementation/Stages/Stages.cs +++ b/src/core/Akka.Streams/Implementation/Stages/Stages.cs @@ -264,6 +264,7 @@ public static class DefaultAttributes /// TBD /// public static readonly Attributes TerminationWatcher = Attributes.CreateName("terminationWatcher"); + public static readonly Attributes Watch = Attributes.CreateName("watch"); /// /// TBD /// diff --git a/src/core/Akka.Streams/WatchedActorTerminatedException.cs b/src/core/Akka.Streams/WatchedActorTerminatedException.cs new file mode 100644 index 00000000000..6e407795ffe --- /dev/null +++ b/src/core/Akka.Streams/WatchedActorTerminatedException.cs @@ -0,0 +1,41 @@ +#region copyright +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2018 Lightbend Inc. +// Copyright (C) 2013-2018 .NET Foundation +// +//----------------------------------------------------------------------- +#endregion + +using System.Runtime.Serialization; +using Akka.Actor; +using Akka.Streams.Dsl; + +namespace Akka.Streams +{ + /// + /// Used as failure exception by an `ask` operator if the target actor terminates. + /// + /// + /// + /// + /// + public class WatchedActorTerminatedException : AkkaException + { + public WatchedActorTerminatedException(string stageName, IActorRef actorRef) + : base($"Actor watched by [{stageName}] has terminated! Was: {actorRef}") + { } + +#if SERIALIZATION + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected WatchedActorTerminatedException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +#endif + } +} \ No newline at end of file diff --git a/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs b/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs index ecfad51e492..42b2e1faa53 100644 --- a/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs +++ b/src/core/Akka.Tests.Shared.Internals/AkkaSpec.cs @@ -131,9 +131,9 @@ protected T ExpectMsgPf(string hint, Func pf) } - protected void Intercept(Action actionThatThrows) where T : Exception + protected T Intercept(Action actionThatThrows) where T : Exception { - Assert.Throws(() => actionThatThrows()); + return Assert.Throws(() => actionThatThrows()); } protected void Intercept(Action actionThatThrows) From 4264c9a16a1ea368ac7d6f5d3f4f53bfe60a8e71 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Tue, 10 Jul 2018 22:49:43 +0200 Subject: [PATCH 12/14] FlowAskSpec: configured test dispatcher --- .../Akka.Streams.Tests/Dsl/FlowAskSpec.cs | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs index 46a30f63092..f479c8a0d11 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading.Tasks; using Akka.Actor; +using Akka.Configuration; using Akka.Streams.Dsl; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; @@ -45,6 +46,8 @@ public bool Equals(Reply other) public override bool Equals(object obj) => obj is Reply reply && Equals(reply); public override int GetHashCode() => Payload; + + public override string ToString() => $"Reply({Payload})"; } sealed class Replier : UntypedActor @@ -116,10 +119,27 @@ public FailOnAllExcept(int n) #endregion + private static readonly Config SpecConfig = ConfigurationFactory.ParseString(@" + akka.test.stream-dispatcher { + type = Dispatcher + executor = ""fork-join-executor"" + fork-join-executor { + parallelism-min = 8 + parallelism-max = 8 + } + mailbox-requirement = ""Akka.Dispatch.IUnboundedMessageQueueSemantics"" + } + + akka.stream { + materializer { + dispatcher = ""akka.test.stream-dispatcher"" + } + }"); + private readonly IMaterializer _materializer; private readonly TimeSpan _timeout = 10.Seconds(); - public FlowAskSpec(ITestOutputHelper output) : base(output) + public FlowAskSpec(ITestOutputHelper output) : base(SpecConfig, output) { _materializer = Sys.Materializer(); } @@ -128,7 +148,7 @@ public FlowAskSpec(ITestOutputHelper output) : base(output) public void Flow_with_ask_must_produce_asked_elements() => this.AssertAllStagesStopped(() => { var replyOnInts = - Sys.ActorOf(Props.Create(() => new Replier()), + Sys.ActorOf(Props.Create(() => new Replier()).WithDispatcher("akka.test.stream-dispatcher"), "replyOnInts"); var c = this.CreateManualSubscriberProbe(); @@ -149,7 +169,7 @@ public void Flow_with_ask_must_produce_asked_elements() => this.AssertAllStagesS [Fact] public void Flow_with_ask_must_produce_asked_elements_for_simple_ask() => this.AssertAllStagesStopped(() => { - var replyOnInts = Sys.ActorOf(Props.Create(() => new Replier()), "replyOnInts"); + var replyOnInts = Sys.ActorOf(Props.Create(() => new Replier()).WithDispatcher("akka.test.stream-dispatcher"), "replyOnInts"); var c = this.CreateManualSubscriberProbe(); var p = Source.From(Enumerable.Range(1, 3)) @@ -169,7 +189,7 @@ public void Flow_with_ask_must_produce_asked_elements_for_simple_ask() => this.A [Fact] public void Flow_with_ask_must_produce_asked_elements_when_response_is_Status_Success() => this.AssertAllStagesStopped(() => { - var statusReplier = Sys.ActorOf(Props.Create(() => new StatusReplier()), "statusReplier"); + var statusReplier = Sys.ActorOf(Props.Create(() => new StatusReplier()).WithDispatcher("akka.test.stream-dispatcher"), "statusReplier"); var c = this.CreateManualSubscriberProbe(); var p = Source.From(Enumerable.Range(1, 3)) @@ -189,7 +209,7 @@ public void Flow_with_ask_must_produce_asked_elements_when_response_is_Status_Su [Fact] public void Flow_with_ask_must_produce_future_elements_in_order() { - var replyRandomDelays = Sys.ActorOf(Props.Create(() => new RandomDelaysReplier()), "replyRandomDelays"); + var replyRandomDelays = Sys.ActorOf(Props.Create(() => new RandomDelaysReplier()).WithDispatcher("akka.test.stream-dispatcher"), "replyRandomDelays"); var c = this.CreateManualSubscriberProbe(); var p = Source.From(Enumerable.Range(1, 50)) @@ -207,7 +227,7 @@ public void Flow_with_ask_must_produce_future_elements_in_order() [Fact] public void Flow_with_ask_must_signal_ask_timeout_failure() => this.AssertAllStagesStopped(() => { - var dontReply = Sys.ActorOf(BlackHoleActor.Props, "dontReply"); + var dontReply = Sys.ActorOf(BlackHoleActor.Props.WithDispatcher("akka.test.stream-dispatcher"), "dontReply"); var c = this.CreateManualSubscriberProbe(); var p = Source.From(Enumerable.Range(1, 50)) @@ -239,7 +259,7 @@ public void Flow_with_ask_must_signal_ask_failure() => this.AssertAllStagesStopp [Fact] public void Flow_with_ask_signal_failure_when_target_actor_is_terminated() => this.AssertAllStagesStopped(() => { - var r = Sys.ActorOf(Props.Create(() => new Replier()), "replyRandomDelays"); + var r = Sys.ActorOf(Props.Create(() => new Replier()).WithDispatcher("akka.test.stream-dispatcher"), "replyRandomDelays"); var done = Source.Maybe() .Ask(r, _timeout, 4) .RunWith(Sink.Ignore(), _materializer); @@ -326,7 +346,7 @@ public void Flow_with_ask_must_resume_after_multiple_failures() => this.AssertAl [Fact] public void Flow_with_ask_should_handle_cancel_properly() => this.AssertAllStagesStopped(() => { - var dontReply = Sys.ActorOf(BlackHoleActor.Props, "dontReply"); + var dontReply = Sys.ActorOf(BlackHoleActor.Props.WithDispatcher("akka.test.stream-dispatcher"), "dontReply"); var pub = this.CreateManualPublisherProbe(); var sub = this.CreateManualSubscriberProbe(); @@ -340,8 +360,12 @@ public void Flow_with_ask_should_handle_cancel_properly() => this.AssertAllStage upstream.ExpectCancellation(); }, _materializer); - private IActorRef ReplierFailOn(int n) => Sys.ActorOf(Props.Create(() => new FailOn(n)), "failureReplier-" + n); + private IActorRef ReplierFailOn(int n) => + Sys.ActorOf(Props.Create(() => new FailOn(n)).WithDispatcher("akka.test.stream-dispatcher"), + "failureReplier-" + n); - private IActorRef ReplierFailAllExceptOn(int n) => Sys.ActorOf(Props.Create(() => new FailOnAllExcept(n)), "failureReplier-" + n); + private IActorRef ReplierFailAllExceptOn(int n) => + Sys.ActorOf(Props.Create(() => new FailOnAllExcept(n)).WithDispatcher("akka.test.stream-dispatcher"), + "failureReplier-" + n); } } \ No newline at end of file From 39d0d0037db0b8d990453836a8bb589bf1cd1805 Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Thu, 2 Aug 2018 18:51:36 +0100 Subject: [PATCH 13/14] added logging for testing purposes of linux netcore issue --- src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs b/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs index f479c8a0d11..d32d4d4f823 100644 --- a/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/FlowAskSpec.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using Akka.Actor; using Akka.Configuration; +using Akka.Event; using Akka.Streams.Dsl; using Akka.Streams.TestKit; using Akka.Streams.TestKit.Tests; @@ -93,8 +94,10 @@ sealed class FailOn : ReceiveActor { public FailOn(int n) { + var log = Context.GetLogger(); Receive(msg => { + log.Info("Incoming message [{0}] of type [{1}]", n, n.GetType()); if (msg == n) Sender.Tell(new Status.Failure(new Exception($"Booming for {n}!"))); else From c940700d492636cfbd4c7b1c5d08d9509f4816fe Mon Sep 17 00:00:00 2001 From: Bartosz Sypytkowski Date: Thu, 15 Nov 2018 23:33:26 +0100 Subject: [PATCH 14/14] Stream ref fixes (#3649) * StreamRefs: fixed deserialization issue * fixed lagging subscription timeouts * API approvals for Akka.Streams --- .../CoreAPISpec.ApproveStreams.approved.txt | 5 +- .../Akka.Streams.Tests/Dsl/StreamRefsSpec.cs | 82 +++++++++++++++++-- src/core/Akka.Streams/Dsl/StreamRefs.cs | 82 +++++++++++++------ .../Serialization/StreamRefSerializer.cs | 25 ++---- src/core/Akka.Streams/StreamRefs.cs | 3 + src/core/Akka.Streams/reference.conf | 9 ++ 6 files changed, 152 insertions(+), 54 deletions(-) diff --git a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt index b3bbf00111b..703c4835401 100644 --- a/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt +++ b/src/core/Akka.API.Tests/CoreAPISpec.ApproveStreams.approved.txt @@ -1829,12 +1829,13 @@ namespace Akka.Streams.Dsl } public sealed class StreamRefSettings { - public StreamRefSettings(int bufferCapacity, System.TimeSpan demandRedeliveryInterval, System.TimeSpan subscriptionTimeout) { } + public StreamRefSettings(int bufferCapacity, System.TimeSpan demandRedeliveryInterval, System.TimeSpan subscriptionTimeout, System.TimeSpan finalTerminationSignalDeadline) { } public int BufferCapacity { get; } public System.TimeSpan DemandRedeliveryInterval { get; } + public System.TimeSpan FinalTerminationSignalDeadline { get; } public string ProductPrefix { get; } public System.TimeSpan SubscriptionTimeout { get; } - public Akka.Streams.Dsl.StreamRefSettings Copy(System.Nullable bufferCapacity = null, System.Nullable demandRedeliveryInterval = null, System.Nullable subscriptionTimeout = null) { } + public Akka.Streams.Dsl.StreamRefSettings Copy(System.Nullable bufferCapacity = null, System.Nullable demandRedeliveryInterval = null, System.Nullable subscriptionTimeout = null, System.Nullable finalTerminationSignalDeadline = null) { } public static Akka.Streams.Dsl.StreamRefSettings Create(Akka.Configuration.Config config) { } public Akka.Streams.Dsl.StreamRefSettings WithBufferCapacity(int value) { } public Akka.Streams.Dsl.StreamRefSettings WithDemandRedeliveryInterval(System.TimeSpan value) { } diff --git a/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs b/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs index ace576233f8..fe499b0b601 100644 --- a/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs +++ b/src/core/Akka.Streams.Tests/Dsl/StreamRefsSpec.cs @@ -7,21 +7,19 @@ //----------------------------------------------------------------------- #endregion -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Akka.Actor; using Akka.Actor.Internal; using Akka.Configuration; using Akka.IO; using Akka.Streams.Dsl; -using Akka.Streams.Implementation; using Akka.Streams.TestKit; using Akka.TestKit; +using FluentAssertions; +using System; +using System.Linq; +using System.Threading; using Xunit; using Xunit.Abstractions; -using FluentAssertions; namespace Akka.Streams.Tests { @@ -100,6 +98,12 @@ protected override bool Receive(object message) sink.PipeTo(Sender); return true; } + case "receive-ignore": + { + var sink = StreamRefs.SinkRef().To(Sink.Ignore()).Run(_materializer); + sink.PipeTo(Sender); + return true; + } case "receive-subscribe-timeout": { var sink = StreamRefs.SinkRef() @@ -306,7 +310,35 @@ public void SourceRef_must_receive_timeout_if_subscribing_too_late_to_the_source // the local "remote sink" should cancel, since it should notice the origin target actor is dead probe.EnsureSubscription(); var ex = probe.ExpectError(); - ex.Message.Should().Contain("has terminated! Tearing down this side of the stream as well."); + ex.Message.Should().Contain("has terminated unexpectedly"); + } + + [Fact] + public void SourceRef_must_not_receive_subscription_timeout_when_got_subscribed() + { + _remoteActor.Tell("give-subscribe-timeout"); + var remoteSource = ExpectMsg>(); + // materialize directly and start consuming, timeout is 500ms + var eventualString = remoteSource.Source + .Throttle(1, 100.Milliseconds(), 1, ThrottleMode.Shaping) + .Take(60) + .RunWith(Sink.Seq(), Materializer); + + eventualString.Wait(8.Seconds()).Should().BeTrue(); + } + + [Fact] + public void SourceRef_must_not_receive_timeout_when_data_is_being_sent() + { + _remoteActor.Tell("give-infinite"); + var remoteSource = ExpectMsg>(); + + var done = remoteSource.Source + .Throttle(1, 200.Milliseconds(), 1, ThrottleMode.Shaping) + .TakeWithin(5.Seconds()) // which is > than the subscription timeout (so we make sure the timeout was cancelled + .RunWith(Sink.Seq(), Materializer); + + done.Wait(8.Seconds()).Should().BeTrue(); } [Fact] @@ -375,6 +407,42 @@ public void SinkRef_must_receive_timeout_if_subscribing_too_late_to_the_sink_ref probe.ExpectCancellation(); } + [Fact] + public void SinkRef_must_not_receive_timeout_if_subscribing_is_already_done_to_the_sink_ref() + { + _remoteActor.Tell("receive-subscribe-timeout"); + var remoteSink = ExpectMsg>(); + Source.Repeat("whatever") + .Throttle(1, 100.Milliseconds(), 1, ThrottleMode.Shaping) + .Take(10) + .RunWith(remoteSink.Sink, Materializer); + + for (int i = 0; i < 10; i++) + { + _probe.ExpectMsg("whatever"); + } + + _probe.ExpectMsg(""); + } + + [Fact] + public void SinkRef_must_not_receive_timeout_while_data_is_being_sent() + { + _remoteActor.Tell("receive-ignore"); + var remoteSink = ExpectMsg>(); + + var done = + Source.Repeat("hello-24934") + .Throttle(1, 300.Milliseconds(), 1, ThrottleMode.Shaping) + .TakeWithin(5.Seconds()) // which is > than the subscription timeout (so we make sure the timeout was cancelled) + .AlsoToMaterialized(Sink.Last(), Keep.Right) + .To(remoteSink.Sink) + .Run(Materializer); + + done.Wait(8.Seconds()).Should().BeTrue(); + + } + [Fact(Skip = "FIXME: how to pass test assertions to remote system?")] public void SinkRef_must_respect_backpressure_implied_by_origin_Sink() { diff --git a/src/core/Akka.Streams/Dsl/StreamRefs.cs b/src/core/Akka.Streams/Dsl/StreamRefs.cs index 50d368018ac..fb66046965f 100644 --- a/src/core/Akka.Streams/Dsl/StreamRefs.cs +++ b/src/core/Akka.Streams/Dsl/StreamRefs.cs @@ -175,14 +175,16 @@ public static StreamRefSettings Create(Config config) return new StreamRefSettings( bufferCapacity: config.GetInt("buffer-capacity", 32), demandRedeliveryInterval: config.GetTimeSpan("demand-redelivery-interval", TimeSpan.FromSeconds(1)), - subscriptionTimeout: config.GetTimeSpan("subscription-timeout", TimeSpan.FromSeconds(30))); + subscriptionTimeout: config.GetTimeSpan("subscription-timeout", TimeSpan.FromSeconds(30)), + finalTerminationSignalDeadline: config.GetTimeSpan("final-termination-signal-deadline", TimeSpan.FromSeconds(2))); } public int BufferCapacity { get; } public TimeSpan DemandRedeliveryInterval { get; } public TimeSpan SubscriptionTimeout { get; } + public TimeSpan FinalTerminationSignalDeadline { get; } - public StreamRefSettings(int bufferCapacity, TimeSpan demandRedeliveryInterval, TimeSpan subscriptionTimeout) + public StreamRefSettings(int bufferCapacity, TimeSpan demandRedeliveryInterval, TimeSpan subscriptionTimeout, TimeSpan finalTerminationSignalDeadline) { BufferCapacity = bufferCapacity; DemandRedeliveryInterval = demandRedeliveryInterval; @@ -197,10 +199,12 @@ public StreamRefSettings(int bufferCapacity, TimeSpan demandRedeliveryInterval, public StreamRefSettings Copy(int? bufferCapacity = null, TimeSpan? demandRedeliveryInterval = null, - TimeSpan? subscriptionTimeout = null) => new StreamRefSettings( + TimeSpan? subscriptionTimeout = null, + TimeSpan? finalTerminationSignalDeadline = null) => new StreamRefSettings( bufferCapacity: bufferCapacity ?? this.BufferCapacity, demandRedeliveryInterval: demandRedeliveryInterval ?? this.DemandRedeliveryInterval, - subscriptionTimeout: subscriptionTimeout ?? this.SubscriptionTimeout); + subscriptionTimeout: subscriptionTimeout ?? this.SubscriptionTimeout, + finalTerminationSignalDeadline: finalTerminationSignalDeadline ?? this.FinalTerminationSignalDeadline); } /// @@ -297,6 +301,11 @@ private sealed class Logic : TimerGraphStageLogic, IInHandler #endregion private Status _completedBeforeRemoteConnected = null; + + // Some when this side of the stream has completed/failed, and we await the Terminated() signal back from the partner + // so we can safely shut down completely; This is to avoid *our* Terminated() signal to reach the partner before the + // Complete/Fail message does, which can happen on transports such as Artery which use a dedicated lane for system messages (Terminated) + private Exception _failedWithAwaitingPartnerTermination = RemoteStreamRefActorTerminatedException.Default; public IActorRef Self => _stageActor.Ref; public IActorRef PartnerRef @@ -323,19 +332,21 @@ public override void PreStart() _stageActor = GetStageActor(InitialReceive); var initialPartnerRef = _stage._initialPartnerRef; if (initialPartnerRef != null) + { + // this will set the `partnerRef` ObserveAndValidateSender(initialPartnerRef, "Illegal initialPartnerRef! This would be a bug in the SinkRef usage or impl."); + TryPull(); + } + else + { + // only schedule timeout timer if partnerRef has not been resolved yet (i.e. if this instance of the Actor + // has not been provided with a valid initialPartnerRef) + ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); + } Log.Debug("Created SinkRef, pointing to remote Sink receiver: {0}, local worker: {1}", initialPartnerRef, Self); _promise.SetResult(new SourceRefImpl(Self)); - - if (_partnerRef != null) - { - _partnerRef.Tell(new OnSubscribeHandshake(Self), Self); - TryPull(); - } - - ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); } private void InitialReceive(Tuple args) @@ -345,10 +356,16 @@ private void InitialReceive(Tuple args) switch (message) { - case Terminated terminated: - if (Equals(terminated.ActorRef, PartnerRef)) - FailStage(new RemoteStreamRefActorTerminatedException($"Remote target receiver of data {PartnerRef} terminated. " + - "Local stream terminating, message loss (on remote side) may have happened.")); + case Terminated terminated when Equals(terminated.ActorRef, PartnerRef): + if (_failedWithAwaitingPartnerTermination == null) + { + // other side has terminated (in response to a completion message) so we can safely terminate + CompleteStage(); + } + else + { + FailStage(_failedWithAwaitingPartnerTermination); + } break; case CumulativeDemand demand: // the other side may attempt to "double subscribe", which we want to fail eagerly since we're 1:1 pairings @@ -385,7 +402,7 @@ protected internal override void OnTimer(object timerKey) { // we know the future has been competed by now, since it is in preStart var ex = new StreamRefSubscriptionTimeoutException($"[{StageActorName}] Remote side did not subscribe (materialize) handed out Sink reference [${_promise.Task.Result}], " + - "within subscription timeout: ${PrettyDuration.format(subscriptionTimeout.timeout)}!"); + $"within subscription timeout: ${SubscriptionTimeout.Timeout}!"); throw ex; // this will also log the exception, unlike failStage; this should fail rarely, but would be good to have it "loud" } @@ -403,8 +420,8 @@ public void OnUpstreamFailure(Exception cause) if (_partnerRef != null) { _partnerRef.Tell(new RemoteStreamFailure(cause.ToString()), Self); - _stageActor.Unwatch(_partnerRef); - FailStage(cause); + _failedWithAwaitingPartnerTermination = cause; + SetKeepGoing(true); // we will terminate once partner ref has Terminated (to avoid racing Terminated with completion message) } else { @@ -420,8 +437,8 @@ public void OnUpstreamFinish() if (_partnerRef != null) { _partnerRef.Tell(new RemoteStreamCompleted(_remoteCumulativeDemandConsumed), Self); - _stageActor.Unwatch(_partnerRef); - CompleteStage(); + _failedWithAwaitingPartnerTermination = null; + SetKeepGoing(true); // we will terminate once partner ref has Terminated (to avoid racing Terminated with completion message) } else { @@ -436,6 +453,8 @@ private void ObserveAndValidateSender(IActorRef partner, string failureMessage) if (_partnerRef == null) { _partnerRef = partner; + partner.Tell(new OnSubscribeHandshake(Self), Self); + CancelTimer(SubscriptionTimeoutKey); _stageActor.Watch(_partnerRef); switch (_completedBeforeRemoteConnected) @@ -443,12 +462,14 @@ private void ObserveAndValidateSender(IActorRef partner, string failureMessage) case Status.Failure failure: Log.Warning("Stream already terminated with exception before remote side materialized, failing now."); partner.Tell(new RemoteStreamFailure(failure.Cause.ToString()), Self); - FailStage(failure.Cause); + _failedWithAwaitingPartnerTermination = failure.Cause; + SetKeepGoing(true); break; case Status.Success _: Log.Warning("Stream already completed before remote side materialized, failing now."); partner.Tell(new RemoteStreamCompleted(_remoteCumulativeDemandConsumed), Self); - CompleteStage(); + _failedWithAwaitingPartnerTermination = null; + SetKeepGoing(true); break; case null: if (!Equals(partner, PartnerRef)) @@ -497,6 +518,7 @@ private sealed class Logic : TimerGraphStageLogic, IOutHandler { private const string SubscriptionTimeoutKey = "SubscriptionTimeoutKey"; private const string DemandRedeliveryTimerKey = "DemandRedeliveryTimerKey"; + private const string TerminationDeadlineTimerKey = "TerminationDeadlineTimerKey"; private readonly SourceRefStageImpl _stage; private readonly TaskCompletionSource> _promise; @@ -560,6 +582,9 @@ public override void PreStart() _promise.SetResult(new SinkRefImpl(Self)); + //this timer will be cancelled if we receive the handshake from the remote SinkRef + // either created in this method and provided as self.ref as initialPartnerRef + // or as the response to first CumulativeDemand request sent to remote SinkRef ScheduleOnce(SubscriptionTimeoutKey, SubscriptionTimeout.Timeout); } @@ -614,6 +639,11 @@ protected internal override void OnTimer(object timerKey) PartnerRef.Tell(new CumulativeDemand(_localCumulativeDemand), Self); ScheduleDemandRedelivery(); break; + case TerminationDeadlineTimerKey: + FailStage(new RemoteStreamRefActorTerminatedException( + $"Remote partner [{PartnerRef}] has terminated unexpectedly and no clean completion/failure message was received " + + $"(possible reasons: network partition or subscription timeout triggered termination of partner). Tearing down.")); + break; } } @@ -653,8 +683,10 @@ private void InitialReceive(Tuple args) break; case Terminated terminated: if (Equals(_partnerRef, terminated.ActorRef)) - FailStage(new RemoteStreamRefActorTerminatedException( - $"The remote partner {terminated.ActorRef} has terminated! Tearing down this side of the stream as well.")); + // we need to start a delayed shutdown in case we were network partitioned and the final signal complete/fail + // will never reach us; so after the given timeout we need to forcefully terminate this side of the stream ref + // the other (sending) side terminates by default once it gets a Terminated signal so no special handling is needed there. + ScheduleOnce(TerminationDeadlineTimerKey, Settings.FinalTerminationSignalDeadline); else FailStage(new RemoteStreamRefActorTerminatedException( $"Received UNEXPECTED Terminated({terminated.ActorRef}) message! This actor was NOT our trusted remote partner, which was: {_partnerRef}. Tearing down.")); diff --git a/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs b/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs index 8d823ba46d2..1bed662d7b4 100644 --- a/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs +++ b/src/core/Akka.Streams/Serialization/StreamRefSerializer.cs @@ -144,26 +144,11 @@ private CumulativeDemand DeserializeCumulativeDemand(byte[] bytes) private SequencedOnNext DeserializeSequenceOnNext(byte[] bytes) { var onNext = Proto.Msg.SequencedOnNext.Parser.ParseFrom(bytes); - var serializer = _serialization.GetSerializerById(onNext.Payload.SerializerId); - object payload; - if (onNext.Payload.MessageManifest != null) - { - var manifest = Encoding.UTF8.GetString(onNext.Payload.MessageManifest.ToByteArray()); - if (serializer is SerializerWithStringManifest s) - { - payload = s.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), manifest); - } - else - { - var type = Type.GetType(manifest, throwOnError: true); - payload = serializer.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), type); - } - } - else - { - payload = serializer.FromBinary(onNext.Payload.EnclosedMessage.ToByteArray(), null); - } - + var p = onNext.Payload; + var payload = _serialization.Deserialize( + p.EnclosedMessage.ToByteArray(), + p.SerializerId, + p.MessageManifest?.ToStringUtf8()); return new SequencedOnNext(onNext.SeqNr, payload); } diff --git a/src/core/Akka.Streams/StreamRefs.cs b/src/core/Akka.Streams/StreamRefs.cs index 422647d1aec..d43661e3e2a 100644 --- a/src/core/Akka.Streams/StreamRefs.cs +++ b/src/core/Akka.Streams/StreamRefs.cs @@ -70,6 +70,9 @@ public StreamRefSubscriptionTimeoutException(string message) : base(message) public sealed class RemoteStreamRefActorTerminatedException : Exception { + internal static readonly RemoteStreamRefActorTerminatedException Default = + new RemoteStreamRefActorTerminatedException("Remote target receiver of data terminated. Local stream terminating, message loss (on remote side) may have happened."); + public RemoteStreamRefActorTerminatedException(string message) : base(message) { } diff --git a/src/core/Akka.Streams/reference.conf b/src/core/Akka.Streams/reference.conf index 26b8f553e52..98f04ba45df 100644 --- a/src/core/Akka.Streams/reference.conf +++ b/src/core/Akka.Streams/reference.conf @@ -106,6 +106,15 @@ akka { # prepare things before it is ready to materialize the reference. However the timeout is needed to avoid leaking # in-active streams which are never subscribed to. subscription-timeout = 30 seconds + + # In order to guard the receiving end of a stream ref from never terminating (since awaiting a Completion or Failed + # message) after / before a Terminated is seen, a special timeout is applied once Terminated is received by it. + # This allows us to terminate stream refs that have been targeted to other nodes which are Downed, and as such the + # other side of the stream ref would never send the "final" terminal message. + # + # The timeout specifically means the time between the Terminated signal being received and when the local SourceRef + # determines to fail itself, assuming there was message loss or a complete partition of the completion signal. + final-termination-signal-deadline = 2 seconds } }