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

Retryable with exponential backoff not working with delayExpression #399

Merged
merged 13 commits into from
Nov 15, 2023
Merged

Retryable with exponential backoff not working with delayExpression #399

merged 13 commits into from
Nov 15, 2023

Conversation

R-Zer0
Copy link
Contributor

@R-Zer0 R-Zer0 commented Nov 8, 2023

No description provided.

@pivotal-cla
Copy link

@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.

@pivotal-cla
Copy link

@R-Zer0 Thank you for signing the Contributor License Agreement!

Copy link
Member

@artembilan artembilan left a 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

@R-Zer0
Copy link
Contributor Author

R-Zer0 commented Nov 8, 2023

Any chances to have this covered by some unit test? See ExponentialBackOffPolicyTests as a candidate for addition. Thanks

Added

return this.interval;
}
else {
return this.interval == DEFAULT_INITIAL_INTERVAL ? this.initialIntervalSupplier.get() : this.interval;
Copy link
Member

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.

Copy link
Contributor Author

@R-Zer0 R-Zer0 Nov 8, 2023

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

Copy link
Member

@artembilan artembilan left a 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 ?

@R-Zer0
Copy link
Contributor Author

R-Zer0 commented Nov 8, 2023

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;
Copy link
Member

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.

Copy link
Member

@artembilan artembilan left a 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 !

@R-Zer0
Copy link
Contributor Author

R-Zer0 commented Nov 9, 2023

@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());
Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor Author

@R-Zer0 R-Zer0 Nov 13, 2023

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);
Copy link
Contributor

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).

Copy link
Contributor Author

@R-Zer0 R-Zer0 Nov 13, 2023

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?

Copy link
Contributor Author

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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

@R-Zer0 R-Zer0 Nov 14, 2023

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;
		}
	}

Copy link
Contributor Author

@R-Zer0 R-Zer0 Nov 14, 2023

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.

Copy link
Member

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.

Copy link
Contributor

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;
}

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated according @garyrussell suggestion.

Copy link
Member

@artembilan artembilan left a 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.

@R-Zer0
Copy link
Contributor Author

R-Zer0 commented Nov 15, 2023

Add your name to @author list, please, of the affected classes.

Requested update pushed

@R-Zer0 R-Zer0 requested a review from artembilan November 15, 2023 07:26
@R-Zer0 R-Zer0 requested a review from garyrussell November 15, 2023 07:26
@artembilan artembilan added the bug label Nov 15, 2023
@artembilan artembilan modified the milestone: 2.0.5 Nov 15, 2023
@artembilan artembilan removed the bug label Nov 15, 2023
@artembilan artembilan merged commit d01f25b into spring-projects:main Nov 15, 2023
1 check passed
@artembilan
Copy link
Member

@R-Zer0 ,

thank you for contribution; looking forward for more!

@R-Zer0 R-Zer0 deleted the fix-retryable-exceptional-backoff-with-delay-expression branch November 17, 2023 11:14
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.

4 participants