Skip to content
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

Ask interface should be clean #3220

Merged
merged 12 commits into from
Jan 30, 2018
Merged

Conversation

maxcherednik
Copy link
Contributor

@maxcherednik maxcherednik commented Dec 21, 2017

This closes #3162, closes #3182
Tests should be precise - in terms of what to expect

  1. It should return a result if Success
  2. It should throw AskTimeoutException if we didn't get the result witin timeout specified
  3. It MIGHT throw TaskCancelledException if we cancel the operation

Plus: we should use the power of the C# itself. Don't block on async functions - .Result and .Wait() are not cool.

var actor = Sys.ActorOf<SomeActor>();
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
Assert.Throws<AggregateException>(() => { actor.Ask<string>("timeout", Timeout.InfiniteTimeSpan, cts.Token).Wait(); });
Assert.True(cts.IsCancellationRequested);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to test CancellationToken source here - it working just fine.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

Assert.True(cts.IsCancellationRequested);
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
{
await Assert.ThrowsAsync<TaskCanceledException>(async () => await actor.Ask<string>("timeout", Timeout.InfiniteTimeSpan, cts.Token));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No mess with the AggregateExceptions and unwrapping... clear well defined expectation!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good

@Horusiath
Copy link
Contributor

@maxcherednik we don't use async tests methods, because they introduced races when the test suite has been run in parallel mode.

{
await Assert.ThrowsAsync<TaskCanceledException>(async () => await actor.Ask<string>("cancel", TimeSpan.FromSeconds(30), cts.Token));
}

Are_Temp_Actors_Removed(actor);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aaronontheweb @Horusiath What is this thing doing here?
We kinda testing Ask here. As far as I can see it should not affect an actor it's used upon.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internally in order to work Ask makes a lightweight temporary actor reference. One of the bugs in the past was that those temporary actors where not recycled after Ask task completed, causing memory leaks.

@maxcherednik
Copy link
Contributor Author

@Horusiath hm... and the reason is ? Cause semantically they are the same.

@Horusiath
Copy link
Contributor

@maxcherednik the problem is not in semantics, but in internals of xunit test runner, which works (or at least it worked in the past) in mysterious ways and did weird things with scheduling tests to be executed. While you won't feel that in simple unit tests, if you do integration tests (which are most of the Akka.NET test suite) it resulted in false negatives, since often assertions were running into timeouts.

@maxcherednik
Copy link
Contributor Author

@Horusiath probably long in the past. We use them all the time all over. Never had any issues of such nature. I thought it's related to Akka.net implementation or something.

So here specifically can we proceed with the modern way?

@Aaronontheweb
Copy link
Member

Looks like AskSpec.Can_get_timeout_when_asking_actor failed on all 3 runtimes.

@maxcherednik
Copy link
Contributor Author

@Aaronontheweb I know, that's the purpose. The problem here is that we were never throwing the AskTimeoutException. Instead we were throwing the TaskCancelledException, which is not really clear.
I've put the info in the description what the interface of the Ask method should be.
If we fix this - it's definitely a breaking change. Question - do we want to fix it to make it beautiful(I actually need it in the context of clear shutdown of the Remoting)?

{
var actor = Sys.ActorOf<SomeActor>();
Assert.Throws<AggregateException>(() => { actor.Ask<string>("timeout", TimeSpan.FromSeconds(3)).Wait(); });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here because we didn't use the async await syntax - we started to catch AggregateException and never actually checked the underneath type. And this probably caused us picking the wrong type of exception to throw(TaskCancelledException instead of AskTimeoutException)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aaronontheweb Look at the code above. It's not actually "to be modern", it's just to help not to hide things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see

@maxcherednik
Copy link
Contributor Author

@Aaronontheweb @Horusiath So if we agree on the interface of the Ask method, I will satisfy the failing test by implementing the right way. Shall I?

@Aaronontheweb
Copy link
Member

I agree with:

It should return a result if Success
It should throw AskTimeoutException if we didn't get the result witin timeout specified
It MIGHT throw TaskCancelledException if we cancel the operation

Don't agree with

Don't block on async functions - .Result and .Wait() are not cool.

Doesn't matter if we block in the test suites or not. As @Horusiath mentioned, we had historical reasons for not doing this in the past. There's no benefit to changing it for the sake of "making it modern." Unless there's a compelling benefit to change something, don't do it.

@Aaronontheweb
Copy link
Member

Go forward with your changes @maxcherednik - makes sense.

maxcherednik added a commit to maxcherednik/akka.net that referenced this pull request Dec 27, 2017
{
IActorRefProvider provider = ResolveProvider(self);
if (provider == null)
throw new ArgumentException("Unable to resolve the target Provider", nameof(self));

return AskEx(self, messageFactory, provider, timeout, cancellationToken).CastTask<object, T>();
return (T)await AskEx(self, messageFactory, provider, timeout, cancellationToken);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this CastTask at all. All we need to do here is to take await and cast the value from object to T.

The rest will be managed by the .net itself. All the exceptions will be propagated as is.

@@ -410,49 +410,60 @@ internal static IActorRefProvider ResolveProvider(ICanTell self)
return null;
}

private static Task<object> AskEx(ICanTell self, Func<IActorRef, object> messageFactory, IActorRefProvider provider, TimeSpan? timeout, CancellationToken cancellationToken)
private static async Task<object> AskEx(ICanTell self, Func<IActorRef, object> messageFactory, IActorRefProvider provider, TimeSpan? timeout, CancellationToken cancellationToken)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this method I did a few changes:

  1. Setting the right exception in case of Timeout
  2. With the help of the async/await - removing one level of indirection(unregister callback is gone) - this logic happens in the try/finaly
  3. Right order of disposing things in the finally block

@@ -123,6 +123,15 @@ protected override void OnReceive(object message)
Are_Temp_Actors_Removed(actor);
}

[Fact]
public async Task ShouldFailWhenAskExpectsWrongType()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra test for InvalidCastException

/// <typeparam name="TResult">TBD</typeparam>
/// <param name="task">TBD</param>
/// <returns>TBD</returns>
public static Task<TResult> CastTask<TTask, TResult>(this Task<TTask> task)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not use this guy anymore, but the features of the C# itself.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

@@ -76,30 +76,29 @@ public class FutureActorRef : MinimalActorRef
/// TBD
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is simplified-the only purpose of this class now is to listen for the response message and put it into the completion source. Less callbacks, more readable code.

maxcherednik added a commit to maxcherednik/akka.net that referenced this pull request Dec 27, 2017
@maxcherednik
Copy link
Contributor Author

maxcherednik commented Dec 27, 2017

@Horusiath @Aaronontheweb got a q.
Why do we need an overload of Task<object> Ask when we have Task<T> Ask<T> which covers the first one? Shall we push the user of this interface to explicitly say what he expects to receive?

@Aaronontheweb
Copy link
Member

@maxcherednik

In general we follow extend-only design principles, so we don't remove stuff once it's been added to the API. Not 100&% always, but usually. I'd just leave the Task<object> overload alone.

@maxcherednik
Copy link
Contributor Author

@Aaronontheweb ok, got it. I don't insist, it's just redundant code with zero value. Was just wondering if there were other hidden design reasons to have it.

@Aaronontheweb
Copy link
Member

I don't insist, it's just redundant code with zero value.

Someone added it a while back and I probably should have said "no" at the time.

@maxcherednik
Copy link
Contributor Author

maxcherednik commented Dec 27, 2017

@Aaronontheweb or @Horusiath could you please review the approach taken and if it's fine, I will add more documentation around Ask interface.

@to11mtm
Copy link
Member

to11mtm commented Jan 1, 2018

If the ask method were made async, would that cause hazards for anyone currently using Ask outside of an asynchronous method?

@maxcherednik
Copy link
Contributor Author

@to11mtm the signature of the method stays the same - that is what's important. The rest is implementation details! We will try to implement it not to break current usages.

@maxcherednik
Copy link
Contributor Author

@Aaronontheweb @Horusiath Could you please review the PR

@Aaronontheweb
Copy link
Member

@maxcherednik sure thing Maxim; sorry for the delay. Had my hands full debugging cluster stuff over the past couple of days!

Copy link
Member

@Aaronontheweb Aaronontheweb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor changes needed. Flagged them in latest review.

var actor = Sys.ActorOf<SomeActor>();
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
Assert.Throws<AggregateException>(() => { actor.Ask<string>("timeout", Timeout.InfiniteTimeSpan, cts.Token).Wait(); });
Assert.True(cts.IsCancellationRequested);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree

@@ -0,0 +1,72 @@
//-----------------------------------------------------------------------
// <copyright file="ArrayExtensions.cs" company="Akka.NET Project">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filename is wrong @maxcherednik

return this;
}

public void GetResult()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there meant to be something here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah... this method there is to make this class awaitable.

/// <typeparam name="TResult">TBD</typeparam>
/// <param name="task">TBD</param>
/// <returns>TBD</returns>
public static Task<TResult> CastTask<TTask, TResult>(this Task<TTask> task)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree


private async Task<object> SendMessage(object message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the redesign of this class. Much more understandable.

@Aaronontheweb
Copy link
Member

One thing I really like about this PR is the performance improvement. I want to see more data from our build server to support this, but....

Before

Totals

Metric Units Max Average Min StdDev
TotalBytesAllocated bytes 6,212,320.00 6,176,605.33 6,129,816.00 42,352.26
[Counter] AskReplies operations 62,455.00 62,455.00 62,455.00 0.00

Per-second Totals

Metric Units / s Max / s Average / s Min / s StdDev / s
TotalBytesAllocated bytes 11,172,576.10 8,357,304.18 5,423,849.57 2,876,184.89
[Counter] AskReplies operations 113,834.29 84,595.72 54,745.32 29,549.23

After

Totals

Metric Units Max Average Min StdDev
TotalBytesAllocated bytes 3,924,808.00 3,917,082.67 3,903,608.00 11,711.15
[Counter] AskReplies operations 115,059.00 115,059.00 115,059.00 0.00

Per-second Totals

Metric Units / s Max / s Average / s Min / s StdDev / s
TotalBytesAllocated bytes 4,244,555.46 4,162,325.65 4,121,139.90 71,213.14
[Counter] AskReplies operations 124,432.66 122,261.02 120,879.65 1,903.78

@Aaronontheweb
Copy link
Member

The steep drop in allocations and the improvement in throughput are correlated. Probably GC behavior.

@maxcherednik
Copy link
Contributor Author

@Aaronontheweb Fixed the header and put a comment in the empty method.
Re: perf - so it got better, right? There used to be the more allocations inside that CastTask method. So not anymore.

@maxcherednik
Copy link
Contributor Author

@Aaronontheweb up:)

@Aaronontheweb
Copy link
Member

Re-running the failed specs...

@Aaronontheweb Aaronontheweb merged commit d233657 into akkadotnet:dev Jan 30, 2018
@Aaronontheweb Aaronontheweb added this to the 1.3.4 milestone Feb 1, 2018
Aaronontheweb pushed a commit that referenced this pull request Feb 1, 2018
* Tests should be precise - in temrs of what to expect

* Ask interface refined #3220

* ClusterRouter unit test fix #3220

* Ask deadlock test added #3220

* Handle deadlock by removing the SynchronizationContext #3220

* Fixing ScatterGather router test #3220

* Ask interface refined #3220

AskSpecs consolidated
Api change approval - internal CastTask removed

* Fixing header #3220
@maxcherednik maxcherednik deleted the askFix branch February 2, 2018 08:27
Aaronontheweb pushed a commit that referenced this pull request Feb 19, 2018
* Tests should be precise - in temrs of what to expect

* Ask interface refined #3220

* ClusterRouter unit test fix #3220

* Ask deadlock test added #3220

* Handle deadlock by removing the SynchronizationContext #3220

* Fixing ScatterGather router test #3220

* Ask interface refined #3220

AskSpecs consolidated
Api change approval - internal CastTask removed

* Fixing header #3220
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Akka.Status.Failure.ToString() throws when Cause is null
4 participants