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

DecorrelatedJitter is a first-class citizen #536

Closed
wants to merge 55 commits into from

Conversation

grant-d
Copy link

@grant-d grant-d commented Nov 14, 2018

ISleepDurationSeriesStrategy providers

Per my design proposal in #526, this PR includes changes that make DecorrelatedJitterBackoff, ConstantBackoff, LinearBackoff and ExponentialBackoff first-class citizens in Polly.

Questions

Details in the linked issue.

  • Any other backoff strategies we need [Edit: No]
  • Is the interface sufficient [Edit: Trimmed]
  • I have added one overload to PolicyBuilder to demonstrate the pattern should we decide to integrate it directly. See additional notes there. I am happy to revert such integration if need be. [Edit: Scoped-out]
  • What about the *Forever overloads, I have not integrated that at all. [Edit: Scoped-out]

Notes

  • The shared Random instance is lock controlled (per separate discussion)
  • I have made lots of comments on the CR to explain what's going on
  • Please forgive a small amount of reformatting - my VS setup is configured to do it automatically. I will revert if necessary.

Usage

Details are in the associated design document. But for quick reference, here's how you would call a strategy. Of course, we may decide to provide direct overloads too:
Here's an example of how to call it. Note that the class is instantiated as a singleton (directly or via DI) and each execution uses the .Generate().

private static readonly ISleepDurationStrategy s_backoff = new DecorrelatedJitterBackoff(MinDelay, MaxDelay);

return Policy<IRestResponse<T>>
    .Handle<HttpRequestException>()
    .OrResult(resultPredicate)
    .WaitAndRetryAsync(s_backoff.Generate(retryCount: 5)) // <-- Note usage
    .ExecuteAsync(() => client.ExecuteTaskAsync<T>(request, cancellationToken));

Risks

  • Mitigated: the use of yield instead of an eager TimeSpan[] array has mitigated the following concern: Inadequate guidance could lead to consumers using the exact same instance of a strategy across multiple calls. It is essential that .Generate is called just-in-time, not pre-generated. (Docs should include info in DecorrelatedJitter is a first-class citizen #536 (comment))

Confirm the following

(I am not sure which of the 2 dev branches to merge from and target) EDIT by @reisenberger : I fixed this and targeted the branch.

  • I have merged the latest changes from the dev vX.Y branch
  • I have successfully run a local build
  • I have included unit tests for the issue/feature
  • I have targeted the PR against the latest dev vX.Y branch

@grant-d
Copy link
Author

grant-d commented Nov 27, 2018

can be enumerated multiple times, giving different randomised results …

I thought about this some more. I think the above would only work in serializable contexts. What would be our guidance when users try to use a shared (eg reified singleton) strategy in concurrent scenarios. In that case you'd need a distinct enumerator per context.

@reisenberger
Copy link
Member

reisenberger commented Nov 27, 2018

can be enumerated multiple times, giving different randomised results …

I think the above would only work in serializable contexts

IIRC the details of yield return implementation, this will be fine whether different enumerations of the enumerable are executing serially or in parallel/concurrently. When the compiler sees a method returning IEnumerable which is implemented with yield return, it (IIRC) compiler-generates a hidden inner class to represent that method returning IEnumerable. Each enumeration instantiates a separate instance of that compiler-generated inner-class. (This actually gives the thing you rightly identify is needed: "a distinct enumerator per [execution]".) That's a quick explanation from memory; blogs should confirm.

So it all works thread-safely - provided, of course, that within our IEnumerable<T>-returning, yield return-implemented method, we don't use anything non-thread-safely. Everything mutable within DecorrelatedJitter.GetSleepDurations(...) has method-execution-scope, and we take care that we use the only shared/external item, _random, thread-safely, so it looks good to me.

@grant-d
Copy link
Author

grant-d commented Nov 27, 2018

this will be fine whether different enumerations of the enumerable are executing serially or in parallel/concurrently ...

You are right, I couldn't recall whether the generated state machine was per-execution or not. I convinced myself using the following code (and associated unit):

            const int count = 10;
            DecorrelatedJitterBackoff durationStrategy = new DecorrelatedJitterBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3));

            IEnumerable<TimeSpan> generate = durationStrategy.GetSleepDurations(count);

            var tasks = new Task<TimeSpan[]>[100];
            var start = new ManualResetEvent(false);

            for (var i = 0; i < tasks.Length; i++)
            {
                var tcs = new TaskCompletionSource<TimeSpan[]>();
                tasks[i] = tcs.Task;

                var thread = new Thread(() =>
                {
                    start.WaitOne();
                    tcs.SetResult(generate.ToArray()); // Every thread enumerates the singleton
                });
                thread.Start();
            }

            start.Set(); // Unleash the fury
            Task.WaitAll(tasks);

            tasks.Should().NotContain(n => n.IsFaulted); // All passed

@reisenberger
Copy link
Member

@grant-d @george-polevoy . Similar to for the current fault-injection work, I am going to propose that we promote this contribution to its own package, in this case (something like) Polly.Contrib.WaitAndRetryStrategies. Advantages:

  • More freedom to rev Polly.Contrib.WaitAndRetryStrategies independently from (not blocked by) the Polly rev cycle - grow/refine more freely as enhancements to the algorithms occur to us/anyone
  • We can include more of @george-polevoy 's algorithm variants (if George is happy to)
  • We can give you greater control over the Polly.Contrib package (if you wish); and headlining as contributors (either way)
  • More space to document the math in depth

Please let me know any thoughts.

Mechanics:

  • I would need time to set up the separate package.
  • We would likely first merge, then-demerge, this PR on Polly in any case, to ensure you get the due contributor credit
  • We would advertise the package directly from the Polly retry wiki page (and everywhere else relevant)

@reisenberger
Copy link
Member

@grant-d The separate package proposal (if agreed) helps guide some of our decisions around interfaces-or-not; and instances-of-a-class configuration versus static factory methods. Propose we may go for:

(a) static factory methods on a separate-class-per-strategy, ie a syntax DecorrelatedJitter.GetSleepDurations(param1, param2, retryCount) , LinearBackoff.GetSleepDurations(....), etc.

  • static class-per-strategy grows more easily than one class holding all variants

(b) static methods, no instances, no interface

Again, let me know if this path forward seems ok.

@grant-d
Copy link
Author

grant-d commented Dec 13, 2018

Sorry, been snowed under at work, will try get to this this week

@reisenberger
Copy link
Member

@grant-d No problem!

@grant-d
Copy link
Author

grant-d commented Jan 4, 2019

Sorry for the long delay, I should have more time now.
I have implemented the factory methods as discussed, including sample units so you can get a feel for the call-site shape.
Your a) vs b) choice is interesting. Considering the semantics of IEnumerable, it may be sufficient to go with b) for what we need. Would you still propose a separate (static) class per strategy?

@grant-d
Copy link
Author

grant-d commented Jan 4, 2019

Regarding core vs contrib. I don't mind either way except for the risk that other contribs I have used in the past have become ill-maintained and/or runaway trains, filled with everyone's favorite idea. As long as we curate both repos strictly, I think we should be fine.

/// For example: 117ms, 236ms, 141ms, 424ms, ...
/// For background, see https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/.
/// </summary>
public static class DecorrelatedJitterBackoff_static // Temporary name
Copy link
Author

Choose a reason for hiding this comment

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

Here's the option where we have static methods returning IEnumerable only


// Static-based instantiation
IEnumerable<TimeSpan> discrete4 = DecorrelatedJitterBackoff_static.Create(minDelay, maxDelay, count, fastFirst, seed).ToList();
discrete4.Should().HaveCount(count);
Copy link
Author

Choose a reason for hiding this comment

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

Sample callsite for static instantiation


// Factory-based instantiation
IEnumerable<TimeSpan> discrete3 = DecorrelatedJitterBackoff.Create(minDelay, maxDelay, count, fastFirst, seed).ToList();
discrete3.Should().HaveCount(count);
Copy link
Author

Choose a reason for hiding this comment

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

Sample callsite for factory instantiation

{
var backoff = new DecorrelatedJitterBackoff(minDelay, maxDelay, fastFirst, seed);
return backoff.GetSleepDurations(retryCount);
}
Copy link
Author

Choose a reason for hiding this comment

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

Here is the new factory method

@reisenberger
Copy link
Member

Thanks @grant-d . I disposed of a question around the new jitter strategy also over the festive season, probably going to be a bit more kanban over next couple of weeks (courtesy update) to focus purely on getting Polly v7.0 and Simmy out, then let's bring this to fruition also. Thanks again!

@reisenberger
Copy link
Member

This PR was unexpectedly automatically closed by github, when deleting a (now-outdated) base branch which the PR targeted .

@grant-d Thank you for everything you have done on this! Now that Polly v7.0.0 is released, I hope to revert shortly to how we may take this forward.

@grant-d
Copy link
Author

grant-d commented Feb 27, 2019

@reisenberger any news on the plans here

@reisenberger
Copy link
Member

@grant-d , I hope to come back to this at the weekend.

@grant-d
Copy link
Author

grant-d commented Feb 28, 2019

Cool. My personal feeling is that class called Alternative is the best approach. I have used such a pattern with Polly in my own code recently, and it's clean.

@reisenberger reisenberger modified the milestones: v7.1.0, v7.2.0 Mar 10, 2019
@reisenberger reisenberger removed this from the v7.1.1 or v7.2.0 milestone Sep 8, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants