question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Cleanly formalise the separation of sync and async policies (was: option to unify them)

See original GitHub issue

TL;DR At the expense of some syntax changes (which would eventually need to be pushed through as breaking changes), we could unify sync and async policies in Polly. What do users think?

A new syntax such as below would be a breaking change, but would allow sync and async policies to be unified:

.WaitAndRetry(int retryCount, Func<int, Context, TimeSpan> sleepDurationProvider)
    .OnRetry(Action<Exception, TimeSpan, int, Context> onRetry) // optional postfix configuration step
    .OnRetryAsync(Func<Exception, TimeSpan, int, Context, Task> onRetryAsync) // optional postfix configuration step

Background: Current sync/async split

With Polly we currently must use separate Polly policies for synchronous and asynchronous executions:

That architecture evolved due to historic decisions (before current maintainers), and this blog post explored some of the reasons and blocks to changing this - to unifying sync and async policies.

Decisive is that users expect async policies to have async state-change delegates, while sync policies must have sync ones. So a policy that would act for both sync and async executions, must at least allow the definition of all state-change delegates in both sync and async forms.

The current syntax - a block to change?

A block to defining policies with both/either sync and async state-change delegates is Polly’s current syntax. Take one of the fuller WaitAndRetry() configuration overloads:

.WaitAndRetry(int retryCount, Func<int, Context, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, int, Context> onRetry).

Add the async state-change delegate, and you have the slightly clumsy:

.WaitAndRetry(int retryCount, Func<int, Context, TimeSpan> sleepDurationProvider, Action<Exception, TimeSpan, int, Context> onRetry, Func<Exception, TimeSpan, int, Context, Task> onRetryAsync)

With policies that take a greater number of state-change delegates (circuit-breaker’s onBreak, onReset and onHalfOpen), overloads quickly become very ugly. And all in addition to existing overloads.

The current syntax - good and bad

The driving factor in the current syntax is that, beyond the .Handle<>() predicates, everything about a policy must be configured in one single overload call.

Negatives:

  • It leads to the current proliferation of configuration overloads.
  • It counts against adding more overloads (therefore more features).
  • It makes overloads for mixed sync/async policies clumsy.

Positives:

  • Configuring everything-in-one-shot means policies are immutable. Immutability is generally good: here, it prevents bugs arising where one part of the code might accidentally change a policy that another part of the code is already using.

New syntax could offer progress?

… (at the expense of breaking changes)

An alternative syntax could keep all the primary characteristics of a policy configured in one shot (they often operate as a unit), while allowing state-change delegates to be configured by fluent postfix:

.WaitAndRetry(int retryCount, Func<int, Context, TimeSpan> sleepDurationProvider)
    .OnRetry(Action<Exception, TimeSpan, int, Context> onRetry) // optional postfix configuration step
    .OnRetryAsync(Func<Exception, TimeSpan, int, Context, Task> onRetryAsync) // optional postfix configuration step

This:

  • drastically reduces the overall number of overloads
  • allows adding both sync and async state-change delegates without creating unwieldy method signatures.

How would state-change delegates on such a policy operate?

If both .OnRetry() and .OnRetryAsync() were configured, a sync execution would use the sync onRetry, an async execution the onRetryAsync().

If only .OnRetry() (sync form) were configured, what should an async execution through the policy do? It could:

(a) invoke no onRetry; or (b) invoke Task.FromResult(onRetry(...)) (or (C#7), with ValueTask) (current preferred solution)

Choice (b) offers some convenience: only one on-retry overload may be configured, but the policy could still be used in both sync and async cases with that on-retry.

If only .OnRetryAsync() were configured, what should a sync execution through the policy do? We cannot invoke an async method and block on it with .Result; we will not introduce Polly blocking on async code. We could:

© silently invoke no on-retry.

Or (d) throw. (current preferred solution: The user might assume the configured onRetryAsync() would be invoked, but it cannot; we should better signal that, with an exception, than silently drop behaviour).

Note that (b) above (whether with © or (d)) gives asymmetric behaviour: async-execution-when-only-sync-state-change-defined would use the state-change delegate, but sync-execution-when-only-async-state-change-defined would omit-state-change-delegate or throw. This asymmetry is probably a price worth paying.

How could we preserve immutability, in the new syntax?

A positive of the existing syntax is policy (/state-change-delegate) immutability. That could still be enforced by:

  • prevent a state-change-delegate being modified if one has already been configured; or
  • prevent a state-change-delegate being modified if any execution has already taken place.

Applicability

While this discussion focuses on retry, the changes should be made across all policy types if implemented. This represents a reasonable amount of work.

User feedback wanted

  • Would Polly users like to see sync/async policies unified in this manner, albeit with a change of syntax?
    • Old overloads could remain included, marked deprecated, for some releases; but eventually they would need removing, as a breaking API change.
  • Should policy (state-change-delegate) immutability be enforced?
  • Other ideas for syntax? Other thoughts?

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:17 (10 by maintainers)

github_iconTop GitHub Comments

4reactions
Im5tucommented, Jul 26, 2017

I would definitely agree that uniformity is a good thing between the api’s, I suggest the approach:

1 - Add in the new apis (in vnext) 2 - Mark the existing apis as obsolete, linking to this discussion and/or a fix (in vnext) 3 - Remove the obsolete apis post vnext

This gives users some time to transition and makes the experience better overall. I wouldn’t keep the old API around for very long as it will hamper productivity of new features etc.

+1 for immutability being enforced

2reactions
reisenbergercommented, Jan 2, 2018

Triage of old issues (@ohadschn, apologies for the delayed reply). Re:

A true compiler guarantee would have Retry and RetryAsync returning different and distinct interfaces

Yes, that’s entirely correct. If we retain the sync/async split in Polly, this is what we should do.

The reason I did not do this is that it would drive the wedge of the sync/async split deeper into Polly (and force a breaking change on users while doing so), when the intention of this work item is to remove it (unify sync/async). I didn’t want to push users in the direction of one breaking change (returning the interface instead of concrete Policy type would be a breaking change for many), only to push them in the opposite direction with another breaking change (unifying sync/async) thereafter.

It’s definitely true to say though that this leaves us - somewhat unsatisfactorily, as you say - only part-way through clearing up the runtime sync/async split inherited when I took over the project. In the meantime, the availability of the interfaces does at least allow users an option to enforce compile-time failure (without breaking changes) if they want to.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Polly - Please use asynchronous-defined policies when ...
I execute the policy like so: Task.Run(() => { foreach (var changeMessage in changeMessages) { policyWrap.ExecuteAsync((context) => ...
Read more >
Using sync and async mode
In deferred mode, the software allows opening files with the scan happening in parallel in the background. The file is allowed only if...
Read more >
(PDF) Creol: A type-safe object-oriented model for distributed ...
This paper considers the problem of reusing synchronization constraints for concurrent objects with asynchronous method calls. Our approach extends the Creol ...
Read more >
Getting Started | Creating Asynchronous Methods
Learn how to create asynchronous service methods.
Read more >
Grant Agreement N° 215483 Title: State of the Art Report, Gap ...
The deliverable presents the state-of-the-art principles, techniques, and methodologies for the monitoring and adaptation of Service-Based Applications.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found