-
Couldn't load subscription status.
- Fork 1.1k
Fixed race conditions + unsafe struct assignment in SelectAsync
#7521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
5786a5c
89cb8f2
dd30ab6
b797906
75bb63c
d70e88f
630a473
1c87c8b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| using Akka.Streams.Util; | ||
| using Akka.Util; | ||
| using Akka.Util.Internal; | ||
| using Debug = System.Diagnostics.Debug; | ||
| using Decider = Akka.Streams.Supervision.Decider; | ||
| using Directive = Akka.Streams.Supervision.Directive; | ||
|
|
||
|
|
@@ -2512,61 +2513,47 @@ public Expand(Func<TIn, IEnumerator<TOut>> extrapolate) | |
| /// </returns> | ||
| public override string ToString() => "Expand"; | ||
| } | ||
|
|
||
| #nullable enable | ||
|
|
||
| /// <summary> | ||
| /// INTERNAL API | ||
| /// </summary> | ||
| /// <typeparam name="TIn">TBD</typeparam> | ||
| /// <typeparam name="TOut">TBD</typeparam> | ||
| [InternalApi] | ||
| public sealed class SelectAsync<TIn, TOut> : GraphStage<FlowShape<TIn, TOut>> | ||
| { | ||
| #region internal classes | ||
|
|
||
| private sealed class Logic : InAndOutGraphStageLogic | ||
| { | ||
| private class Holder<T> | ||
| private sealed class Holder<T>(object? message, Result<T> element) | ||
| { | ||
| private readonly Action<Holder<T>> _callback; | ||
|
|
||
| public Holder(object message, Result<T> element, Action<Holder<T>> callback) | ||
| { | ||
| _callback = callback; | ||
| Message = message; | ||
| Element = element; | ||
| } | ||
|
|
||
| public Result<T> Element { get; private set; } | ||
| public object Message { get; } | ||
| public object? Message { get; private set; } = message; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| public Result<T> Element { get; private set; } = element; | ||
|
|
||
| public void SetElement(Result<T> result) | ||
| { | ||
| Element = result.IsSuccess && result.Value == null | ||
| ? Result.Failure<T>(ReactiveStreamsCompliance.ElementMustNotBeNullException) | ||
| : result; | ||
| } | ||
|
|
||
| public void Invoke(Result<T> result) | ||
| { | ||
| SetElement(result); | ||
| _callback(this); | ||
| } | ||
| } | ||
|
|
||
| private static readonly Result<TOut> NotYetThere = Result.Failure<TOut>(new Exception()); | ||
|
|
||
| private readonly SelectAsync<TIn, TOut> _stage; | ||
| private readonly Decider _decider; | ||
| private IBuffer<Holder<TOut>> _buffer; | ||
| private readonly Action<Holder<TOut>> _taskCallback; | ||
| private readonly Action<(Holder<TOut>, Result<TOut>)> _taskCallback; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AsyncCallbacks in Akka.Streams can only accept a single input parameter, so we have to go with a |
||
|
|
||
| public Logic(Attributes inheritedAttributes, SelectAsync<TIn, TOut> stage) : base(stage.Shape) | ||
| { | ||
| _stage = stage; | ||
| var attr = inheritedAttributes.GetAttribute<ActorAttributes.SupervisionStrategy>(null); | ||
| var attr = inheritedAttributes.GetAttribute<ActorAttributes.SupervisionStrategy>(); | ||
| _decider = attr != null ? attr.Decider : Deciders.StoppingDecider; | ||
|
|
||
| _taskCallback = GetAsyncCallback<Holder<TOut>>(HolderCompleted); | ||
| _taskCallback = GetAsyncCallback<(Holder<TOut> holder, Result<TOut> result)>(t => HolderCompleted(t.holder, t.result)); | ||
|
|
||
| SetHandlers(stage.In, stage.Out, this); | ||
| } | ||
|
|
@@ -2577,19 +2564,33 @@ public override void OnPush() | |
| try | ||
| { | ||
| var task = _stage._mapFunc(message); | ||
| var holder = new Holder<TOut>(message, NotYetThere, _taskCallback); | ||
| var holder = new Holder<TOut>(message, NotYetThere); | ||
| _buffer.Enqueue(holder); | ||
|
|
||
| // We dispatch the task if it's ready to optimize away | ||
| // scheduling it to an execution context | ||
| if (task.IsCompleted) | ||
| { | ||
| holder.SetElement(Result.FromTask(task)); | ||
| HolderCompleted(holder); | ||
| HolderCompleted(holder, Result.FromTask(task)); | ||
| } | ||
| else | ||
| task.ContinueWith(t => holder.Invoke(Result.FromTask(t)), | ||
| TaskContinuationOptions.ExecuteSynchronously); | ||
| { | ||
| async Task WaitForTask() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used the latest .NET meta for relying on a local function + detached There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has the side effect of eliminating the |
||
| { | ||
| try | ||
| { | ||
| var result = Result.Success(await task); | ||
| _taskCallback((holder, result)); | ||
| } | ||
| catch(Exception ex){ | ||
| var result = Result.Failure<TOut>(ex); | ||
| _taskCallback((holder, result)); | ||
| } | ||
| } | ||
|
|
||
| _ = WaitForTask(); | ||
| } | ||
|
|
||
| } | ||
| catch (Exception e) | ||
| { | ||
|
|
@@ -2606,7 +2607,7 @@ public override void OnPush() | |
| break; | ||
|
|
||
| default: | ||
| throw new AggregateException($"Unknown SupervisionStrategy directive: {strategy}", e); | ||
| throw new ArgumentOutOfRangeException($"Unknown SupervisionStrategy directive: {strategy}", e); | ||
| } | ||
| } | ||
| if (Todo < _stage._parallelism && !HasBeenPulled(_stage.In)) | ||
|
|
@@ -2663,12 +2664,12 @@ private void PushOne() | |
| break; | ||
|
|
||
| default: | ||
| throw new AggregateException($"Unknown SupervisionStrategy directive: {strategy}", result.Exception); | ||
| throw new ArgumentOutOfRangeException($"Unknown SupervisionStrategy directive: {strategy}", result.Exception); | ||
| } | ||
| continue; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The whole error handling code inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we decided in Slack that we probably still need error-handling in both places since there can be a delay on |
||
|
|
||
| Push(_stage.Out, result.Value); | ||
| Push(_stage.Out!, result.Value); | ||
| if (Todo < _stage._parallelism && !HasBeenPulled(inlet)) | ||
| TryPull(inlet); | ||
| } | ||
|
|
@@ -2677,17 +2678,18 @@ private void PushOne() | |
| } | ||
| } | ||
|
|
||
| private void HolderCompleted(Holder<TOut> holder) | ||
| private void HolderCompleted(Holder<TOut> holder, Result<TOut> result) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the real fix - we now pass in both the |
||
| { | ||
| var element = holder.Element; | ||
| if (element.IsSuccess) | ||
| // we may not be at the front of the line right now, so save the result for later | ||
| holder.SetElement(result); | ||
| if (result.IsSuccess) | ||
| { | ||
| if (IsAvailable(_stage.Out)) | ||
| PushOne(); | ||
| return; | ||
| } | ||
|
|
||
| var exception = element.Exception; | ||
| var exception = result.Exception; | ||
| var strategy = _decider(exception); | ||
| Log.Error(exception, "An exception occured inside SelectAsync while executing Task. Supervision strategy: {0}", strategy); | ||
| switch (strategy) | ||
|
|
@@ -2703,7 +2705,7 @@ private void HolderCompleted(Holder<TOut> holder) | |
| break; | ||
|
|
||
| default: | ||
| throw new AggregateException($"Unknown SupervisionStrategy directive: {strategy}", exception); | ||
| throw new ArgumentOutOfRangeException($"Unknown SupervisionStrategy directive: {strategy}", exception); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -2758,8 +2760,6 @@ protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) | |
| /// <summary> | ||
| /// INTERNAL API | ||
| /// </summary> | ||
| /// <typeparam name="TIn">TBD</typeparam> | ||
| /// <typeparam name="TOut">TBD</typeparam> | ||
| [InternalApi] | ||
| public sealed class SelectAsyncUnordered<TIn, TOut> : GraphStage<FlowShape<TIn, TOut>> | ||
| { | ||
|
|
@@ -2904,6 +2904,8 @@ public SelectAsyncUnordered(int parallelism, Func<TIn, Task<TOut>> mapFunc) | |
| protected override GraphStageLogic CreateLogic(Attributes inheritedAttributes) | ||
| => new Logic(inheritedAttributes, this); | ||
| } | ||
|
|
||
| #nullable disable | ||
|
|
||
| /// <summary> | ||
| /// INTERNAL API | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enable
nullableto take advantage of the changes made in #7520