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

Make DelegatingServiceInstanceListSupplier implement SelectedInstanceCallback #1221

Closed
HJK181 opened this issue Mar 27, 2023 · 14 comments · Fixed by #1226
Closed

Make DelegatingServiceInstanceListSupplier implement SelectedInstanceCallback #1221

HJK181 opened this issue Mar 27, 2023 · 14 comments · Fixed by #1226
Assignees
Milestone

Comments

@HJK181
Copy link
Contributor

HJK181 commented Mar 27, 2023

I've tried to setup a custom load balancer as described in this post. However, with spring-cloud-starter-loadbalancer:3.1.4 I'm experiencing

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier' available: more than one 'primary' bean found among candidates: [CustomServiceInstanceListSupplier, retryAwareDiscoveryClientServiceInstanceListSupplier]
@Configuration
@LoadBalancerClient(value = "custom", configuration = CustomLoadBalancerConfiguration.class)
public class CustomLoadBalancerClient {
}

public class CustomLoadBalancerConfiguration {

	@Bean
	@Primary
	public ServiceInstanceListSupplier customServiceInstanceListSupplier(ConfigurableApplicationContext context, RedissonClient redissonClient) {
		ServiceInstanceListSupplier delegate = ServiceInstanceListSupplier.builder()
				.withDiscoveryClient()
				.withCaching()
				.build(context);
		return new CustomServiceInstanceListSupplier(delegate, redissonClient);
	}
}

public class CustomServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier implements SelectedInstanceCallback {
...
}

When deploying the application to Kubernetes along spring-cloud-kubernetes-facric8-loadbalancer:2.1.4 and activating spring.cloud.kubernetes.loadbalancer.mode=SERVICE it's even getting worse

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier' available: more than one 'primary' bean found among candidates: [customServiceInstanceListSupplier, kubernetesServicesListSupplier, retryAwareDiscoveryClientServiceInstanceListSupplier]

Am I doing something wrong, or is the example from the post wrong?

@HJK181
Copy link
Contributor Author

HJK181 commented Mar 27, 2023

Removing @Primary leads to

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier' available: expected single matching bean but found 2: customServiceInstanceListSupplier,kubernetesServicesListSupplier

@HJK181
Copy link
Contributor Author

HJK181 commented Mar 28, 2023

Further debugging revealed a second issue. My CustomServiceInstanceListSupplier implements SelectedInstanceCallback which the RoundRobinLoadBalancer respects by calling SelectedInstanceCallback#selectedServiceInstance

public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
...
private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
			List<ServiceInstance> serviceInstances) {
		Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
		if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
			((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
		}
		return serviceInstanceResponse;
	}
}

However, with Spring retry on the classpath my CustomServiceInstanceListSupplier is wrapped in a RetryAwareServiceInstanceListSupplier which is not respecting SelectedInstanceCallback on its delegate.

@Configuration(proxyBeanMethods = false)
	@ConditionalOnBlockingDiscoveryEnabled
	@ConditionalOnClass(RetryTemplate.class)
	@Conditional(BlockingOnAvoidPreviousInstanceAndRetryEnabledCondition.class)
	@AutoConfigureAfter(BlockingSupportConfiguration.class)
	@ConditionalOnBean(ServiceInstanceListSupplier.class)
	public static class BlockingRetryConfiguration {

		@Bean
		@ConditionalOnBean(DiscoveryClient.class)
		@Primary
		public ServiceInstanceListSupplier retryAwareDiscoveryClientServiceInstanceListSupplier(
				ServiceInstanceListSupplier delegate) {
			return new RetryAwareServiceInstanceListSupplier(delegate);
		}

	}

I suspect that every implementation of ServiceInstanceListSupplier using some sort of "delegate" has the same problem, see RequestBasedStickySessionServiceInstanceListSupplier as an example.

@HJK181
Copy link
Contributor Author

HJK181 commented Mar 31, 2023

@OlgaMaciaszek any feedback appreciated as I'm stuck here. What I need to achieve is that my own ServiceInstanceListSupplier is used regardless of any auto-configured supplier. Or I need a way to use the auto-configured one as a delegate in my supplier, but that won't work as retryAwareDiscoveryClientServiceInstanceListSupplier is marked @Primary.

@HJK181
Copy link
Contributor Author

HJK181 commented Apr 12, 2023

@spencergibb Can you please have a look and give me hint if this is considered a bug or if any configuration possibility exists as a workaround? I've tried using @AutoConfigureAfter(LoadBalancerClientConfiguration.ReactiveRetryConfiguration.class) at my class annotated with @LoadBalancerClient without a chance. retryAwareDiscoveryClientServiceInstanceListSupplier is still configured after mine ...

@OlgaMaciaszek
Copy link
Collaborator

Hello, @HJK181. This is happening as for retrying failed requests, RetryAwareServiceInstanceListSupplier is enabled by default. As indicated in the docs, if you do not want it to be instantiated, you should set spring.cloud.loadbalancer.retry.avoidPreviousInstance to false. Does this solve your issue?

@HJK181
Copy link
Contributor Author

HJK181 commented Apr 12, 2023

I’d like to use the retry functionality, but in conjunction with my custom instance list supplier.

@OlgaMaciaszek
Copy link
Collaborator

That property does not disable retry. It only disables the RetryAwareServiceInstanceListSupplier that handles avoiding retrying on the same instance.

@HJK181
Copy link
Contributor Author

HJK181 commented Apr 12, 2023

Yeah but that’s a good thing I would prefer to not turn off. Particularly for all other load balancer clients in my spring cloud gateway (route based).

What I actually need is wrapping the RetryAwareServiceInstanceListSupplier in my custom supplier as a delegate but still have the primary RetryAwareServiceInstanceListSupplier for all other LB clients. Or as I noted above, all supplier that use a delegate should respect SelectedInstanceCallback on the delegate. This way my custom supplier can work as a delegate of RetryAwareServiceInstanceListSupplier

@OlgaMaciaszek
Copy link
Collaborator

@HJK181, that makes sense, however, the way this hierarchy has been designed is that the supplier provided by the user in a custom config (or our default one) is used as the delegate of the RetryAwareServiceInstanceListSupplier and not the other way around. Maybe you could rearchitect your solution to work in such a fashion? Since we haven't seen any similar issues, I don't think we'll be considering a change in this implementation at this point.

Alternatively, RetryAwareServiceInstanceListSupplier is public, so you might want to disable its automated instantiation via the property and then make your ServiceInstanceSupplier implementation extend it and execute your additional logic against the instance set returned after executing get() on the supertype.

@HJK181
Copy link
Contributor Author

HJK181 commented Apr 12, 2023

@OlgaMaciaszek Thank you for looking into it. Let me propose a solution that from my POV should fit into the current design while improving the overall functionality. The design allows one to create a custom configuration for an LB client and provide a custom implementation of ServiceInstanceListSupplier. However, the design does not correctly handle the case that the custom implementation implements SelectedInstanceCallback as the custom ServiceInstanceListSupplier might be used as a delegate, for example by RetryAwareServiceInstanceListSupplier.

To solve this the abstract DelegatingServiceInstanceListSupplier which is the base class of RetryAwareServiceInstanceListSupplier and hopefully all other ServiceInstanceListSupplier that use a delegate should just implement SelectedInstanceCallback and check if the delegate is an instanceof SelectedInstanceCallback like so

public abstract class DelegatingServiceInstanceListSupplier
		implements ServiceInstanceListSupplier, InitializingBean, DisposableBean, SelectedInstanceCallback {
....
@Override
public void selectedServiceInstance(ServiceInstance serviceInstance) {
  if (delegate instanceof SelectedInstanceCallback selectedInstanceCallbackDelegate) {
    selectedInstanceCallbackDelegate.selectedServiceInstance(serviceInstance);
  }
}
}

What do you think?

@OlgaMaciaszek
Copy link
Collaborator

OlgaMaciaszek commented Apr 13, 2023

@HJK181 That looks reasonable; we could put it in 4.1.x; would you like to submit a PR with that?

@HJK181
Copy link
Contributor Author

HJK181 commented Apr 13, 2023

I can check tomorrow if I find the time to setup my IDE for contributing to the project. Is there any chance that it will be backported to 3.1.x?

@OlgaMaciaszek
Copy link
Collaborator

Unfortunately, not really. We mostly only backport bugfixes. This would be a new feature.

HJK181 added a commit to HJK181/spring-cloud-commons that referenced this issue Apr 14, 2023
respect SelectedInstanceCallback on its delegate. Motivation behind is
that some load balancer implementations like the RoundRobinLoadBalancer
to call selectedServiceInstance on the ServiceInstanceListSupplier after
choosing the next instance. However, when a ServiceInstanceListSupplier
in wrapped by the DelegatingServiceInstanceListSupplier this
functionality breaks, as DelegatingServiceInstanceListSupplier is not
implementing SelectedInstanceCallback and therfore does ignoring that its
delegate might also do. This might break the functionality of the
delegate. Fixes spring-cloudgh-1221.
@HJK181
Copy link
Contributor Author

HJK181 commented Apr 19, 2023

@OlgaMaciaszek #1226 FYI

@OlgaMaciaszek OlgaMaciaszek added this to the 4.0.4 milestone May 30, 2023
@OlgaMaciaszek OlgaMaciaszek moved this to In Progress in 2022.0.4 May 30, 2023
@OlgaMaciaszek OlgaMaciaszek moved this to In Progress in 2023.0.0-M1 May 30, 2023
@github-project-automation github-project-automation bot moved this from In Progress to Done in 2022.0.4 May 31, 2023
@github-project-automation github-project-automation bot moved this from In Progress to Done in 2023.0.0-M1 May 31, 2023
@OlgaMaciaszek OlgaMaciaszek changed the title Custom configuration for the LoadBalancer doesn't work Make DelegatingServiceInstanceListSupplier implement SelectedInstanceCallback Jul 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Status: Done
Status: Done
Development

Successfully merging a pull request may close this issue.

4 participants