1

I'm currently updating from Spring boot 2.2.x to 2.6.x + legacy code, it's a big jump so there were multiple changes. I'm now running into a problem with load balancing through an api-gateway. I'll apologize in advance for the wall of code to come. I will put the point of failure at the bottom.

When I send in an API request, I get the following error:

more than one 'primary' bean found among candidates: [zookeeperDiscoveryClientServiceInstanceListSupplier, serviceInstanceListSupplier, retryAwareDiscoveryClientServiceInstanceListSupplier]

it seems that the zookeeperDiscovery and retryAware suppliers are loaded through the default serviceInsatnceListSupplier, which has @Primary over it. I thought would take precedence over the other ones. I assume I must be doing something wrong due changes in the newer version, here are the relevant code in question:

@Configuration
@LoadBalancerClients(defaultConfiguration = ClientConfiguration.class)
public class WebClientConfiguration {

   @Bean
   @Qualifier("microserviceWebClient")
   @ConditionalOnMissingBean(name = "microserviceWebClient")
   public WebClient microserviceWebClient(@Qualifier("microserviceWebClientBuilder") WebClient.Builder builder) {
      return builder.build();
   }

   @Bean
   @Qualifier("microserviceWebClientBuilder")
   @ConditionalOnMissingBean(name = "microserviceWebClientBuilder")
   @LoadBalanced
   public WebClient.Builder microserviceWebClientBuilder() {
      return WebClient.builder();
   }


   @Bean
   @Primary
   public ReactorLoadBalancerExchangeFilterFunction reactorLoadBalancerExchangeFilterFunction(
      ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerFactory) {
      //the transformer is currently null, there wasn't a transformer before the upgrade
      return new CustomExchangeFilterFunction(loadBalancerFactory, transformer);
   }
}

There are also some Feign Client related configs here which I will omit, since it's not (or shouldn't be) playing a role in this problem:

public class ClientConfiguration {
   /**
    * The property key within the feign clients configuration context for the feign client name.
    */
   public static final String FEIGN_CLIENT_NAME_PROPERTY = "feign.client.name";

   public ClientConfiguration() {

   }

   //Creates a new BiPredicate for shouldClose. This will be used to determine if HTTP Connections should be automatically closed or not. 
   @Bean
   @ConditionalOnMissingBean
   public BiPredicate<Response, Type> shouldClose() {
      return (Response response, Type type) -> {
         if(type instanceof Class) {
            Class<?> currentClass = (Class<?>) type;
            return (null == AnnotationUtils.getAnnotation(currentClass, EnableResponseStream.class));
         }
         return true;
      };
   }

   //Creates a Custom Decoder
   @Bean
   public Decoder createCustomDecoder(
      ObjectFactory<HttpMessageConverters> converters, BiPredicate<Response, Type> shouldClose
   ) {
      return new CustomDecoder(converters, shouldClose);
   }

   @Bean
   @Qualifier("loadBalancerName")
   public String loadBalancerName(PropertyResolver propertyResolver) {
      String name = propertyResolver.getProperty(FEIGN_CLIENT_NAME_PROPERTY);
      if(StringUtils.hasText(name)) {
         // we are in a feign context
         return name;
      }

      // we are in a LoadBalancerClientFactory context
      name = propertyResolver.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
      Assert.notNull(name, "Could not find a load balancer name within the configuration context!");

      return name;
   }


   @Bean
   public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(
      BeanFactory beanFactory, @Qualifier("loadBalancerName") String loadBalancerName
   ) {
      return new CustomRoundRobinLoadBalancer(
         beanFactory.getBeanProvider(ServiceInstanceListSupplier.class),
         loadBalancerName
      );
   }

    

   @Bean
   @Primary
   public ServiceInstanceListSupplier serviceInstanceListSupplier(
      @Qualifier(
         "filter"
      ) Predicate<ServiceInstance> filter, DiscoveryClient discoveryClient, Environment environment, @Qualifier(
         "loadBalancerName"
      ) String loadBalancerName
   ) {
      // add service name to environment if necessary
      if(environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME) == null) {
         StandardEnvironment wrapped = new StandardEnvironment();

         if(environment instanceof ConfigurableEnvironment) {
            ((ConfigurableEnvironment) environment).getPropertySources()
               .forEach(s -> wrapped.getPropertySources().addLast(s));
         }

         Map<String, Object> additionalProperties = new HashMap<>();
         additionalProperties.put(LoadBalancerClientFactory.PROPERTY_NAME, loadBalancerName);
         wrapped.getPropertySources().addLast(new MapPropertySource(loadBalancerName, additionalProperties));

         environment = wrapped;
      }

      return new FilteringInstanceListSupplier(filter, discoveryClient, environment);
   }
}

There was a change in the ExchangeFilter constructor, but as far as I can tell, it accepts that empty transformer,I don't know if it's supposed to:

public class CustomExchangeFilterFunction extends ReactorLoadBalancerExchangeFilterFunction {
   private static final ThreadLocal<ClientRequest> REQUEST_HOLDER = new ThreadLocal<>();
   //I think it's wrong but I don't know what to do here
   private static List<LoadBalancerClientRequestTransformer> transformersList; 
   private final Factory<ServiceInstance> loadBalancerFactory;



   public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory) {
       this(loadBalancerFactory);
       

   
   ///according to docs, but I don't know where and if I need to use this
   @Bean
   public LoadBalancerClientRequestTransformer transformer() {
       return new LoadBalancerClientRequestTransformer() {
           @Override
           public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
               return ClientRequest.from(request)
                       .header(instance.getInstanceId())
                       .build();
           }
       };
   }
   
   public CustomExchangeFilterFunction (Factory<ServiceInstance> loadBalancerFactory, List<LoadBalancerClientRequestTransformer> transformersList) {
      super(loadBalancerFactory, transformersList); //the changed constructor
      
      this.loadBalancerFactory = loadBalancerFactory;;
   }


   @Override
   public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
      // put the current request into the thread context - ugly, but couldn't find a better way to access the request within
      // the choose method without reimplementing nearly everything
      REQUEST_HOLDER.set(request);
      try {
         return super.filter(request, next);
      } finally {
         REQUEST_HOLDER.remove();
      }
   }


   //used to be an override, but the function has changed
   //code execution doesn't even get this far yet
   protected Mono<Response<ServiceInstance>> choose(String serviceId) {
      ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerFactory.getInstance(serviceId);
      if(loadBalancer == null) {
         return Mono.just(new EmptyResponse());
      }

      ClientRequest request = REQUEST_HOLDER.get();

      // this might be null, if the underlying implementation changed and this method is no longer executed in the same
      // thread
      // as the filter method
      Assert.notNull(request, "request must not be null, underlying implementation seems to have changed");

      return choose(loadBalancer, filter);
   }

   protected Mono<Response<ServiceInstance>> choose(
      ReactiveLoadBalancer<ServiceInstance> loadBalancer,
      Predicate<ServiceInstance> filter
   ) {
      return Mono.from(loadBalancer.choose(new DefaultRequest<>(filter)));
   }
}

There were pretty big changes in the CustomExchangeFilterFunction, but the current execution doesn't even get there. It fails here, in .getIfAvailable(...):

public class CustomRoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {

   private static final int DEFAULT_SEED_POSITION = 1000;

   private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
   private final String serviceId;
   private final int seedPosition;
   private final AtomicInteger position;
   private final Map<String, AtomicInteger> positionsForVersions = new HashMap<>();

   public CustomRoundRobinLoadBalancer (
      ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
      String serviceId
   ) {
      this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(DEFAULT_SEED_POSITION));
   }

   public CustomRoundRobinLoadBalancer (
      ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
      String serviceId,
      int seedPosition
   ) {
      Assert.notNull(serviceInstanceListSupplierProvider, "serviceInstanceListSupplierProvider must not be null");
      Assert.notNull(serviceId, "serviceId must not be null");

      this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
      this.serviceId = serviceId;
      this.seedPosition = seedPosition;
      this.position = new AtomicInteger(seedPosition);
   }


   @Override
   // we have no choice but to use the raw type Request here, because this method overrides another one with this signature
   public Mono<Response<ServiceInstance>> choose(@SuppressWarnings("rawtypes") Request request) {
     //fails here!
      ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
         .getIfAvailable(NoopServiceInstanceListSupplier::new);

      return supplier.get().next().map((List<ServiceInstance> instances) -> getInstanceResponse(instances, request));
   }
}

Edit: after some deeper stacktracing, it seems that it does go into the CustomFilterFunction and invokes the constructor with super(loadBalancerFactory, transformer)

Pompompurin
  • 165
  • 3
  • 14

1 Answers1

0

I found the problem or a workaround. I was using @LoadBalancerClients because I thought it would just set the same config for all clients that way (even if I technically only have one atm). I changed it to @LoadBalancerClient and it suddenly worked. I don't quite understand why this made a difference but it did!

Pompompurin
  • 165
  • 3
  • 14