-
Notifications
You must be signed in to change notification settings - Fork 519
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
Retryable with exponential backoff not working with delayExpression #399
Retryable with exponential backoff not working with delayExpression #399
Conversation
@R-Zer0 Please sign the Contributor License Agreement! Click here to manually synchronize the status of this Pull Request. See the FAQ for frequently asked questions. |
@R-Zer0 Thank you for signing the Contributor License Agreement! |
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.
Any chances to have this covered by some unit test?
See ExponentialBackOffPolicyTests
as a candidate for addition.
Thanks
Added |
src/main/java/org/springframework/retry/backoff/ExponentialBackOffPolicy.java
Outdated
Show resolved
Hide resolved
return this.interval; | ||
} | ||
else { | ||
return this.interval == DEFAULT_INITIAL_INTERVAL ? this.initialIntervalSupplier.get() : this.interval; |
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.
Don't we need to call intervalSupplier
every time?
Looks like with your change we do that only once.
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.
Only first time when we need initial value, further iteration store multiplayed value in the interval
variable here
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.
The build against your PR has failed.
Would you mind double checking it locally running ./mvnw clean verify
?
I found discrepancy between annotation default delay value 1000 and here 1000 too but here it is 100 I suspect this value also should be 1000, pushing this change. Let me know if my understanding correct and this change acceptable. |
* 'multiplier' value this gives a useful initial spread of pauses for 1-5 retries. | ||
*/ | ||
public static final long DEFAULT_INITIAL_INTERVAL = 100L; | ||
public static final long DEFAULT_INITIAL_INTERVAL = 1000L; |
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.
This is some kind of breaking change for the current point release, but I indeed see the reasoning behind consistency.
Plus it looks like the @Backoff
annotation is always contributing exactly that 1000
value.
So, we really can bite a bullet and let this pass through to the next release in a couple weeks.
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.
LGTM.
Let's hear yet from @garyrussell !
@artembilan pushed minor update to if else logic, to make it more readable |
return (long) (this.interval * getMultiplier()); | ||
} | ||
else { | ||
return (long) (this.initialIntervalSupplier.get() * getMultiplier()); |
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.
This is incorrect - the multiplier must be applied to the previous interval; this always applies it to the initial interval.
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.
Yes, that's why there is that logic in the if
: || this.interval != DEFAULT_INITIAL_INTERVAL
.
So, the initialIntervalSupplier
is called only once, for the first time. The next interval
is really based on the previous one from now on.
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.
I've made this by purpose: during first iteration we should check if supplier provided or not, if provided, first iteration should use value provided by initialIntervalSupplier
(if present), further it will use interval
value since it will be assigned to interval
here
BackOffContext context = strategy.start(null); | ||
for (int x = 0; x < 5; x++) { | ||
strategy.backOff(context); | ||
assertThat(sleeper.getLastBackOff()).isEqualTo(seed); |
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.
We need to test that we still have exponential back offs (which we no longer have with the current change).
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.
Could you suggest any example here?
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.
Actually current test checking following, validated in debug.
Last backoff | Seed |
---|---|
40 | 40 |
48 | 48 |
57 | 57 |
68 | 68 |
81 | 81 |
I was thinking it is sufficient for current issue.
} | ||
|
||
public double getMultiplier() { | ||
return this.multiplierSupplier != null ? this.multiplierSupplier.get() : this.multiplier; | ||
} | ||
|
||
public long getInterval() { | ||
return this.intervalSupplier != null ? this.intervalSupplier.get() : this.interval; | ||
if (initialIntervalSupplier == null || this.interval != DEFAULT_INITIAL_INTERVAL) { |
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.
Why are we changing the logic to make the interval
superior to the intervalSupplier
?
Perhaps a description of exactly what you are trying to "fix" in the PR description would have been helpful.
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.
It is not superior, actually It is fix to use intervalSupplier as initial value only, as this code was written. Because it was always incrementing interval
on every iteration, regardless supplier provided or not.
Sorry adding description here, this my first time I am pushing to spring repo :)
So idea was to make interval multiplication work properly. And use supplier only to obtain initial value, to keep the same logic to operate interval
variable
So I updated code to use correct saved multiplied value on every iteration(previously it was always multiplying interval
but using intervalSuplier for sleep value which was always constant initial value)
So I changed it that on first iteration we check if initial value provided by supplier or interval
, than multiplied and stored in the interval
variable and on the second and others iterations we are switching to interval
variable.
Hope this explanation is clear
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.
Sorry, I am still missing something; with the existing code...
return this.intervalSupplier != null ? this.intervalSupplier.get() : this.interval;
...the supplier, if present, takes precedence over the interval
, regardless of whether it is the default value or not.
Also, it is not at all clear to me why we are testing against DEFAULT_INITIAL_INTERVAL
- this value could be reached at some point after the initial delay - e.g. if the initial interval returned by the supplier is DEFAULT_INITIAL_INTERVAL / 2
and the multiplier
is 2.0.
Perhaps you could provide a test case that fails with the existing code, it would help me understand what you are trying to fix.
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.
Sorry didn't understand line of code your referring, is it before my change?
You can try test I added in this PR it is failing without my change.
@Test
public void testMultiBackOffWithInitialDelaySupplier() {
ExponentialBackOffPolicy strategy = new ExponentialBackOffPolicy();
long seed = 40;
double multiplier = 1.2;
strategy.initialIntervalSupplier(() -> 40L);
strategy.setMultiplier(multiplier);
strategy.setSleeper(sleeper);
BackOffContext context = strategy.start(null);
for (int x = 0; x < 5; x++) {
strategy.backOff(context);
assertThat(sleeper.getLastBackOff()).isEqualTo(seed);
seed *= multiplier;
}
}
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.
If we are talking about old code and you referring to getInterval() method we calling it at line 280 so it will always returns supplier. But in the line 286 we calling getNextInterval() and assigning value to interval
(not supplier) . Next iteration supplier will provide same initial value instead of incremented stored in interval
on previous iteration. So we end up, when backoff always have initial value and interval
always multiplied and never actually used.
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.
The problem, @garyrussell , that getSleepAndIncrement()
when calling this.interval = getNextInterval();
does not take into account intervalSupplier
.
And this one we need to call there only once: for initial interval
value since this.interval
is not set in case of intervalSupplier
.
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.
Ok; I see the problem now, thanks; but I think this is a much simpler fix (and doesn't require any tests against the constant which, as I stated, may cause undesirable side effects).
public long getInterval() {
if (this.intervalSupplier != null) {
this.interval = this.intervalSupplier.get();
this.intervalSupplier = null;
}
return this.interval;
}
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.
... and I guess we need to set its result into this.interval
since getNextInterval()
is going to use that one only.
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.
Yes, 3rd line in the above code.
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.
updated according @garyrussell suggestion.
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.
Add your name to @author
list, please, of the affected classes.
Requested update pushed |
@R-Zer0 , thank you for contribution; looking forward for more! |
No description provided.