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

Why and When use ExecuteAsync or Execute? [what happens when you execute an async delegate through a sync policy] #483

Closed
vany0114 opened this Issue Jul 13, 2018 · 6 comments

Comments

Projects
None yet
3 participants
@vany0114

vany0114 commented Jul 13, 2018

Hi guys,

I already read your article explaining synchronous vs asynchronous policies, and I understand your point, but in the end, it looks like there is no difference if I use either ExecuteAsync or Execute method, in the below example I'm executing an async method with Sync and Async policies and it works ok in both cases, so what's the difference, why I would use one or another, I'm confused with the behavior.

private async Task<bool> DoSomehitngAsync()
{
	await Task.Delay(TimeSpan.FromSeconds(1));
	return true;
}

public async Task TestAsync()
{
	var policy = Policy
		.Handle<Exception>()
		.RetryAsync(3);

	await policy.ExecuteAsync(async () =>
	{
		var something = await DoSomehitngAsync();
		Assert.AreEqual(true, something);
	});
}

public async void TestSync()
{
	var policy = Policy
		.Handle<Exception>()
		.Retry(3);

	await policy.Execute(async () =>
	{
		var something = await DoSomehitngAsync();
		Assert.AreEqual(true, something);
	});
}
@reisenberger

This comment has been minimized.

Show comment
Hide comment
@reisenberger

reisenberger Jul 13, 2018

Member

@vany0114 You are seeing no difference in the specific example you posted because the delegates executed through the policy do not fault. Try this code and you'll see a difference. The key difference is that the executed delegate faults (but few enough times that the policy should handle it). Incidental (not significant) differences are:

  • I changed from Assert.Fail(...) only to quickly run this up as a console app.
  • I changed the signature of TestSync() to async Task so that we could await it properly. (Won't go into a side discussion here about the perils of async void which you may know anyway.)

I'll post a follow-up post in a moment explaining why TestSync() fails (but post back if it's immediately self-evident to you, on seeing this!)

    class Program
    {
        static async Task Main(string[] args)
        {
            var toTestSync = new PollySyncAsyncTest();
            var toTestAsync = new PollySyncAsyncTest();

            await toTestSync.TestSync();
            await toTestAsync.TestAsync();
        }
    }

    class PollySyncAsyncTest
    {
        private int counter = 0;

        public async Task TestAsync()
        {
            var policy = Policy
                .Handle<Exception>()
                .RetryAsync(3);

            await policy.ExecuteAsync(async () =>
            {
                var something = await DoSomethingAsync();
                if (something != true) throw new Exception("Assert fail");
            });
        }

        public async Task TestSync()
        {
            var policy = Policy
                .Handle<Exception>()
                .Retry(3);

            await policy.Execute( // Executing a Func<Task> delegate through a sync policy.
                async () => // Providing the compiler an async delegate where it expects a sync delegate.
            {
                var something = await DoSomethingAsync();
                if (something != true) throw new Exception("Assert fail");
            });
        }

        private async Task<bool> DoSomethingAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(1));

            if (++counter <= 2) throw new Exception("Fault which we might expect the policy to handle");

            return true;
        }
    }
Member

reisenberger commented Jul 13, 2018

@vany0114 You are seeing no difference in the specific example you posted because the delegates executed through the policy do not fault. Try this code and you'll see a difference. The key difference is that the executed delegate faults (but few enough times that the policy should handle it). Incidental (not significant) differences are:

  • I changed from Assert.Fail(...) only to quickly run this up as a console app.
  • I changed the signature of TestSync() to async Task so that we could await it properly. (Won't go into a side discussion here about the perils of async void which you may know anyway.)

I'll post a follow-up post in a moment explaining why TestSync() fails (but post back if it's immediately self-evident to you, on seeing this!)

    class Program
    {
        static async Task Main(string[] args)
        {
            var toTestSync = new PollySyncAsyncTest();
            var toTestAsync = new PollySyncAsyncTest();

            await toTestSync.TestSync();
            await toTestAsync.TestAsync();
        }
    }

    class PollySyncAsyncTest
    {
        private int counter = 0;

        public async Task TestAsync()
        {
            var policy = Policy
                .Handle<Exception>()
                .RetryAsync(3);

            await policy.ExecuteAsync(async () =>
            {
                var something = await DoSomethingAsync();
                if (something != true) throw new Exception("Assert fail");
            });
        }

        public async Task TestSync()
        {
            var policy = Policy
                .Handle<Exception>()
                .Retry(3);

            await policy.Execute( // Executing a Func<Task> delegate through a sync policy.
                async () => // Providing the compiler an async delegate where it expects a sync delegate.
            {
                var something = await DoSomethingAsync();
                if (something != true) throw new Exception("Assert fail");
            });
        }

        private async Task<bool> DoSomethingAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(1));

            if (++counter <= 2) throw new Exception("Fault which we might expect the policy to handle");

            return true;
        }
    }
@reisenberger

This comment has been minimized.

Show comment
Hide comment
@reisenberger

reisenberger Jul 13, 2018

Member

To answer your original question:

Why and When use ExecuteAsync or Execute?

Use async policies and ExecuteAsync(...) any time you are executing an async delegate.

Why? In other words, why does TestSync() in the above example fail?

At the line marked // Providing the compiler an async delegate ... the code provides the compiler an async delegate where it expects a sync one. The compiler doesn't complain. An async modifier isn't actually intrinsically part of the delegate signature, it doesn't cause a compile error due to method signature mismatch. All an async modifier does is say "the delegate can contain await statements". And modifies the return signature of the delegate: in this case from void to Task. (An async modifier doesn't even make a delegate run asynchronously, it just (broadly) does those two previously mentioned things.) For more background on these nuances (if unfamiliar) check out articles/blogs by Stephen Cleary and Stephen Toub. So your whole executed async () => delegate here is - as far as the compiler is concerned - just a Func<Task>.

So at the line marked policy.Execute( // Executing a Func<Task> delegate through a sync policy, the code is actually doing policy.Execute<Task>(Func<Task> foo). It's saying: execute that delegate through the policy and give me back the Task the delegate returns.

The final piece of understanding relies on knowing what await actually does. When execution hits an await statement, it immediately returns from the method synchronously, returning a Task representing the ongoing work. So what happens when you await policy.Execute<Task>(Func<Task> foo) is this:

  • policy executes the Func<Task> that is your delegate
  • as soon as execution hits the first await, in var something = await DoSomethingAsync();, the delegate synchronously (see above) returns the Task representing the rest of the execution.
  • no exception has been thrown up to this point, the delegate has successfully returned a Task, so as far as policy is concerned, the execution has completed with success. policy has finished its work (it executed a synchronous delegate and that delegate returned synchronously without an exception yet), so policy returns that Task to the calling code and plays no further part in the execution.
  • back in your calling code, you then await that returned Task.
  • one second later that Task throws. The calling code is awaiting a Task that throws, so it bombs out rethrows.

Does that help?


EDIT: To look at this another way, we can look at why it works with the async policy and not with the sync policy.

  • If we look at the internals of the async policy, we see it is (as you would expect) executing the user delegate with await. So it awaits the Task that represents doing the user-delegate work (after the first await is hit). So it captures the exception thrown at if (++counter <= 2) throw new Exception(...);. So the rest of the policy can handle that exception.
  • If we look at the internals of the sync policy, it is (as you would expect) executing the user delegate without await. So (when an async delegate is executed through a sync policy) it immediately returns the Task which represents doing the user-delegate work (after the first await is hit). So it doesn't govern the exception thrown at if (++counter <= 2) ...

In general the moral of the story is: don't mix sync and async code - particularly where exception-handling is involved.

Member

reisenberger commented Jul 13, 2018

To answer your original question:

Why and When use ExecuteAsync or Execute?

Use async policies and ExecuteAsync(...) any time you are executing an async delegate.

Why? In other words, why does TestSync() in the above example fail?

At the line marked // Providing the compiler an async delegate ... the code provides the compiler an async delegate where it expects a sync one. The compiler doesn't complain. An async modifier isn't actually intrinsically part of the delegate signature, it doesn't cause a compile error due to method signature mismatch. All an async modifier does is say "the delegate can contain await statements". And modifies the return signature of the delegate: in this case from void to Task. (An async modifier doesn't even make a delegate run asynchronously, it just (broadly) does those two previously mentioned things.) For more background on these nuances (if unfamiliar) check out articles/blogs by Stephen Cleary and Stephen Toub. So your whole executed async () => delegate here is - as far as the compiler is concerned - just a Func<Task>.

So at the line marked policy.Execute( // Executing a Func<Task> delegate through a sync policy, the code is actually doing policy.Execute<Task>(Func<Task> foo). It's saying: execute that delegate through the policy and give me back the Task the delegate returns.

The final piece of understanding relies on knowing what await actually does. When execution hits an await statement, it immediately returns from the method synchronously, returning a Task representing the ongoing work. So what happens when you await policy.Execute<Task>(Func<Task> foo) is this:

  • policy executes the Func<Task> that is your delegate
  • as soon as execution hits the first await, in var something = await DoSomethingAsync();, the delegate synchronously (see above) returns the Task representing the rest of the execution.
  • no exception has been thrown up to this point, the delegate has successfully returned a Task, so as far as policy is concerned, the execution has completed with success. policy has finished its work (it executed a synchronous delegate and that delegate returned synchronously without an exception yet), so policy returns that Task to the calling code and plays no further part in the execution.
  • back in your calling code, you then await that returned Task.
  • one second later that Task throws. The calling code is awaiting a Task that throws, so it bombs out rethrows.

Does that help?


EDIT: To look at this another way, we can look at why it works with the async policy and not with the sync policy.

  • If we look at the internals of the async policy, we see it is (as you would expect) executing the user delegate with await. So it awaits the Task that represents doing the user-delegate work (after the first await is hit). So it captures the exception thrown at if (++counter <= 2) throw new Exception(...);. So the rest of the policy can handle that exception.
  • If we look at the internals of the sync policy, it is (as you would expect) executing the user delegate without await. So (when an async delegate is executed through a sync policy) it immediately returns the Task which represents doing the user-delegate work (after the first await is hit). So it doesn't govern the exception thrown at if (++counter <= 2) ...

In general the moral of the story is: don't mix sync and async code - particularly where exception-handling is involved.

@vany0114

This comment has been minimized.

Show comment
Hide comment
@vany0114

vany0114 Jul 13, 2018

@reisenberger thanks for the great explanation! I just wanted to be aware in order to use them properly.

vany0114 commented Jul 13, 2018

@reisenberger thanks for the great explanation! I just wanted to be aware in order to use them properly.

@reisenberger

This comment has been minimized.

Show comment
Hide comment
@reisenberger

reisenberger Jul 14, 2018

Member

@vany0114 np. Thanks for the great question!

Member

reisenberger commented Jul 14, 2018

@vany0114 np. Thanks for the great question!

@reisenberger reisenberger changed the title from Why and When use ExecuteAsync or Execute? to Why and When use ExecuteAsync or Execute? [what happens when you execute an async delegate through a sync policy] Jul 14, 2018

@cmeeren

This comment has been minimized.

Show comment
Hide comment
@cmeeren

cmeeren Aug 30, 2018

I had the same question and this is a great explanation @reisenberger. Should definitely be added to the wiki!

cmeeren commented Aug 30, 2018

I had the same question and this is a great explanation @reisenberger. Should definitely be added to the wiki!

@reisenberger

This comment has been minimized.

Show comment
Hide comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment