Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ public ServiceInstanceListSupplier healthCheckDiscoveryClientServiceInstanceList
return ServiceInstanceListSupplier.builder().withDiscoveryClient().withHealthChecks().build(context);
}

@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
@Conditional(MultiAZFailoverConfigurationCondition.class)
public ServiceInstanceListSupplier multiAZFailoverDiscoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withDiscoveryClient().withMultiAZFailover().withCaching()
.build(context);
}

@Bean
@ConditionalOnBean(ReactiveDiscoveryClient.class)
@ConditionalOnMissingBean
Expand Down Expand Up @@ -221,6 +231,16 @@ public ServiceInstanceListSupplier healthCheckRestClientDiscoveryClientServiceIn
.build(context);
}

@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
@Conditional(MultiAZFailoverConfigurationCondition.class)
public ServiceInstanceListSupplier multiAZFailoverServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder().withBlockingDiscoveryClient().withMultiAZFailover()
.withCaching().build(context);
}

@Bean
@ConditionalOnBean(DiscoveryClient.class)
@ConditionalOnMissingBean
Expand Down Expand Up @@ -386,6 +406,16 @@ public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata)

}

static class MultiAZFailoverConfigurationCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return LoadBalancerEnvironmentPropertyUtils.equalToForClientOrDefault(context.getEnvironment(),
"configurations", "multi-az-failover");
}

}

static class RequestBasedStickySessionConfigurationCondition implements Condition {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package org.springframework.cloud.loadbalancer.config;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

Expand All @@ -37,23 +38,27 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;

/**
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
* @author Jiwon Jeon
*/
@Configuration(proxyBeanMethods = false)
@LoadBalancerClients
@EnableConfigurationProperties({ LoadBalancerClientsProperties.class, LoadBalancerEagerLoadProperties.class })
@AutoConfigureBefore({ ReactorLoadBalancerClientAutoConfiguration.class,
LoadBalancerBeanPostProcessorAutoConfiguration.class })
@EnableConfigurationProperties({LoadBalancerClientsProperties.class, LoadBalancerEagerLoadProperties.class})
@AutoConfigureBefore({ReactorLoadBalancerClientAutoConfiguration.class,
LoadBalancerBeanPostProcessorAutoConfiguration.class})
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.enabled", havingValue = "true", matchIfMissing = true)
public class LoadBalancerAutoConfiguration {

@Bean
@ConditionalOnMissingBean
public LoadBalancerZoneConfig zoneConfig(Environment environment) {
return new LoadBalancerZoneConfig(environment.getProperty("spring.cloud.loadbalancer.zone"));
return new LoadBalancerZoneConfig(environment.getProperty("spring.cloud.loadbalancer.zone"),
Arrays.stream(StringUtils.commaDelimitedListToStringArray(environment.getProperty("spring.cloud.loadbalancer.secondary-zones", "")))
.toList());
}

@ConditionalOnMissingBean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.loadbalancer.config;

import com.github.benmanes.caffeine.cache.Caffeine;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;
import org.springframework.cloud.loadbalancer.core.LoadBalancerCacheDataManager;
import org.springframework.cloud.loadbalancer.core.MultiAZFailoverLoadBalancerCacheDataManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

/**
* An AutoConfiguration that provides a {@link LoadBalancerCacheDataManager} bean for
* adding ServiceInstance into a cache.
*
* @author Jiwon Jeon
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(LoadBalancerCacheAutoConfiguration.class)
@AutoConfigureBefore(LoadBalancerLifecycleConfiguration.class)
public class LoadBalancerCacheDataManagerConfiguration {

@Bean
@DependsOn("caffeineLoadBalancerCacheManager")
@ConditionalOnClass({ Caffeine.class, CaffeineCacheManager.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "multi-az-failover")
LoadBalancerCacheDataManager multiAZFailoverCaffeineLoadBalancerCacheDataManager(ApplicationContext context) {
return new MultiAZFailoverLoadBalancerCacheDataManager(
context.getBean("caffeineLoadBalancerCacheManager", LoadBalancerCacheManager.class));
}

@Bean
@DependsOn("defaultLoadBalancerCacheManager")
@Conditional(OnCaffeineCacheMissingCondition.class)
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "multi-az-failover")
LoadBalancerCacheDataManager multiAZFailoverDefaultLoadBalancerCacheDataManager(ApplicationContext context) {
return new MultiAZFailoverLoadBalancerCacheDataManager(
context.getBean("defaultLoadBalancerCacheManager", LoadBalancerCacheManager.class));
}

static final class OnCaffeineCacheMissingCondition extends AnyNestedCondition {

private OnCaffeineCacheMissingCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}

@ConditionalOnMissingClass("com.github.benmanes.caffeine.cache.Caffeine")
static class CaffeineClassMissing {

}

@ConditionalOnMissingClass("org.springframework.cache.caffeine.CaffeineCacheManager")
static class CaffeineCacheManagerClassMissing {

}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.loadbalancer.config;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.loadbalancer.core.LoadBalancerCacheDataManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* An AutoConfiguration that provides {@link LoadBalancerLifecycle} bean defining
* behaviors before and after load-balancing.
*
* @author Jiwon Jeon
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter({ LoadBalancerCacheAutoConfiguration.class, LoadBalancerCacheDataManagerConfiguration.class })
public class LoadBalancerLifecycleConfiguration {

@Bean
@ConditionalOnBean(LoadBalancerCacheDataManager.class)
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.configurations", havingValue = "multi-az-failover")
LoadBalancerLifecycle multiAZFailoverLoadBalancerLifecycle(LoadBalancerCacheDataManager cacheDataManager) {
return new MultiAZFailoverLoadBalancerLifecycle(cacheDataManager);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@

package org.springframework.cloud.loadbalancer.config;

import java.util.Collections;
import java.util.List;

/**
* @author Olga Maciaszek-Sharma
* @author Jiwon Jeon
*/
public class LoadBalancerZoneConfig {

Expand All @@ -27,8 +31,15 @@ public class LoadBalancerZoneConfig {
*/
private String zone;

private List<String> secondaryZones;

public LoadBalancerZoneConfig(String zone) {
this.zone = zone;
this(zone, Collections.EMPTY_LIST);
}

public LoadBalancerZoneConfig(String primaryZone, List<String> secondaryZones) {
this.zone = primaryZone;
this.secondaryZones = secondaryZones;
}

public String getZone() {
Expand All @@ -39,4 +50,12 @@ public void setZone(String zone) {
this.zone = zone;
}

public List<String> getSecondaryZones() {
return secondaryZones;
}

public void setSecondaryZones(List<String> secondaryZones) {
this.secondaryZones = secondaryZones;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.loadbalancer.config;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.CompletionContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.client.loadbalancer.ResponseData;
import org.springframework.cloud.loadbalancer.core.LoadBalancerCacheDataManager;
import org.springframework.web.reactive.result.view.RequestContext;

/**
* A LoadBalancerLifecycle implementation that defines behaviors before and after
* load-balancing, considering Multi-AZ failover.
*
* @author Jiwon Jeon
*/
public class MultiAZFailoverLoadBalancerLifecycle
implements LoadBalancerLifecycle<RequestContext, ResponseData, ServiceInstance> {

private static final Log LOG = LogFactory.getLog(MultiAZFailoverLoadBalancerLifecycle.class);

private final LoadBalancerCacheDataManager cacheDataManager;

public MultiAZFailoverLoadBalancerLifecycle(LoadBalancerCacheDataManager cacheDataManager) {
this.cacheDataManager = cacheDataManager;
}

@Override
public boolean supports(Class requestContextClass, Class responseClass, Class serverTypeClass) {
return RequestContext.class.isAssignableFrom(requestContextClass)
&& ResponseData.class.isAssignableFrom(responseClass)
&& ServiceInstance.class.isAssignableFrom(serverTypeClass);
}

@Override
public void onStart(Request<RequestContext> request) {
}

@Override
public void onStartRequest(Request<RequestContext> request, Response<ServiceInstance> lbResponse) {
}

@Override
public void onComplete(CompletionContext<ResponseData, ServiceInstance, RequestContext> completionContext) {
final CompletionContext.Status status = completionContext.status();
final ServiceInstance instance = completionContext.getLoadBalancerResponse().getServer();
final ResponseData clientResponse = completionContext.getClientResponse();

if (CompletionContext.Status.DISCARD.equals(status)) {
if (LOG.isWarnEnabled()) {
LOG.warn("Any instance selected. Cache will be evicted...");
}

cacheDataManager.clear();
}
else if (CompletionContext.Status.FAILED.equals(status)
&& (clientResponse == null || clientResponse.getHttpStatus().is5xxServerError())) {
if (LOG.isErrorEnabled()) {
LOG.error(String.format("Requesting to [%s] failed", instance));
}

cacheDataManager.putInstance(instance);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.cloud.loadbalancer.core;

import org.springframework.cache.Cache;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager;

/**
* An interface allows adding ServiceInstance into cache through
* {@link LoadBalancerCacheManager}.
*
* @author Jiwon Jeon
*/
public interface LoadBalancerCacheDataManager {

Cache getCache();

void clear();

<T> T getInstance(String key, Class<T> classType);

void putInstance(ServiceInstance instance);

}
Loading