Adds a weight filter to add weighted routing to groups of routes.

fixes gh-67
This commit is contained in:
Spencer Gibb
2018-03-01 16:34:34 -05:00
parent 85461d41b2
commit 41a981c4bb
15 changed files with 768 additions and 6 deletions

View File

@@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
import org.springframework.cloud.gateway.filter.ForwardRoutingFilter;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter;
import org.springframework.cloud.gateway.filter.NettyRoutingFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter;
@@ -81,6 +82,7 @@ import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFac
import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
import org.springframework.cloud.gateway.route.CachingRouteLocator;
import org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator;
import org.springframework.cloud.gateway.route.CompositeRouteLocator;
@@ -96,8 +98,10 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Primary;
import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
@@ -323,6 +327,11 @@ public class GatewayAutoConfiguration {
return new WebsocketRoutingFilter(webSocketClient, webSocketService, headersFilters);
}
@Bean
public WeightCalculatorWebFilter weightCalculatorWebFilter(Validator validator) {
return new WeightCalculatorWebFilter(validator);
}
/*@Bean
//TODO: default over netty? configurable
public WebClientHttpRoutingFilter webClientHttpRoutingFilter() {
@@ -387,6 +396,12 @@ public class GatewayAutoConfiguration {
return new RemoteAddrRoutePredicateFactory();
}
@Bean
@DependsOn("weightCalculatorWebFilter")
public WeightRoutePredicateFactory weightRoutePredicateFactory() {
return new WeightRoutePredicateFactory();
}
// GatewayFilter Factory beans
@Bean

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2013-2018 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
*
* http://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.gateway.event;
import org.springframework.cloud.gateway.support.WeightConfig;
import org.springframework.context.ApplicationEvent;
public class WeightDefinedEvent extends ApplicationEvent {
private final WeightConfig weightConfig;
public WeightDefinedEvent(Object source, WeightConfig weightConfig) {
super(source);
this.weightConfig = weightConfig;
}
public WeightConfig getWeightConfig() {
return weightConfig;
}
}

View File

@@ -0,0 +1,236 @@
/*
* Copyright 2013-2018 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
*
* http://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.gateway.filter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.cloud.gateway.event.PredicateArgsEvent;
import org.springframework.cloud.gateway.event.WeightDefinedEvent;
import org.springframework.cloud.gateway.support.ConfigurationUtils;
import org.springframework.cloud.gateway.support.WeightConfig;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.style.ToStringCreator;
import org.springframework.validation.Validator;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.WEIGHT_ATTR;
import reactor.core.publisher.Mono;
/**
* @author Spencer Gibb
*/
public class WeightCalculatorWebFilter implements WebFilter, Ordered, SmartApplicationListener {
private static final Log log = LogFactory.getLog(WeightCalculatorWebFilter.class);
public static final int WEIGHT_CALC_FILTER_ORDER = 10001;
private final Validator validator;
private Random random = new Random();
private int order = WEIGHT_CALC_FILTER_ORDER;
private Map<String, GroupWeightConfig> groupWeights = new ConcurrentHashMap<>();
/* for testing */ WeightCalculatorWebFilter() {
this(null);
}
public WeightCalculatorWebFilter(Validator validator) {
this.validator = validator;
}
@Override
public int getOrder() {
return order;
}
public void setOrder(int order) {
this.order = order;
}
public void setRandom(Random random) {
this.random = random;
}
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return PredicateArgsEvent.class.isAssignableFrom(eventType) || // config file
WeightDefinedEvent.class.isAssignableFrom(eventType); // java dsl
}
@Override
public boolean supportsSourceType(Class<?> sourceType) {
return true;
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof PredicateArgsEvent) {
handle((PredicateArgsEvent) event);
} else if (event instanceof WeightDefinedEvent) {
addWeightConfig(((WeightDefinedEvent)event).getWeightConfig());
}
}
public void handle(PredicateArgsEvent event) {
Map<String, Object> args = event.getArgs();
if (args.isEmpty() || !hasRelevantKey(args)) {
return;
}
WeightConfig config = new WeightConfig(event.getRouteId());
ConfigurationUtils.bind(config, args,
WeightConfig.CONFIG_PREFIX, WeightConfig.CONFIG_PREFIX, validator);
addWeightConfig(config);
}
private boolean hasRelevantKey(Map<String, Object> args) {
return args.keySet().stream()
.anyMatch(key -> key.startsWith(WeightConfig.CONFIG_PREFIX + "."));
}
/* for testing */ void addWeightConfig(WeightConfig weightConfig) {
String group = weightConfig.getGroup();
GroupWeightConfig c = groupWeights.get(group);
if (c == null) {
c = new GroupWeightConfig(group);
groupWeights.put(group, c);
}
GroupWeightConfig config = c;
config.weights.put(weightConfig.getRouteId(), weightConfig.getWeight());
//recalculate
// normalize weights
int weightsSum = config.weights.values().stream().mapToInt(Integer::intValue).sum();
final AtomicInteger index = new AtomicInteger(0);
config.weights.forEach((routeId, weight) -> {
Double nomalizedWeight = weight / (double) weightsSum;
config.normalizedWeights.put(routeId, nomalizedWeight);
// recalculate rangeIndexes
config.rangeIndexes.put(index.getAndIncrement(), routeId);
});
//TODO: calculate ranges
config.ranges.clear();
config.ranges.add(0.0);
List<Double> values = new ArrayList<>(config.normalizedWeights.values());
for (int i = 0; i < values.size(); i++) {
Double currentWeight = values.get(i);
Double previousRange = config.ranges.get(i);
Double range = previousRange + currentWeight;
config.ranges.add(range);
}
if (log.isTraceEnabled()) {
log.trace("Recalculated group weight config "+ config);
}
}
/* for testing */ Map<String, GroupWeightConfig> getGroupWeights() {
return groupWeights;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Map<String, String> weights = getWeights(exchange);
groupWeights.forEach((group, config) -> {
double r = this.random.nextDouble();
List<Double> ranges = config.ranges;
if (log.isTraceEnabled()) {
log.trace("Weight for group: "+group +", ranges: "+ranges +", r: "+r);
}
for (int i = 0; i < ranges.size() - 1; i++) {
if (r >= ranges.get(i) && r < ranges.get(i+1)) {
String routeId = config.rangeIndexes.get(i);
weights.put(group, routeId);
break;
}
}
});
if (log.isTraceEnabled()) {
log.trace("Weights attr: "+weights);
}
return chain.filter(exchange);
}
@NotNull
/* for testing */ static Map<String, String> getWeights(ServerWebExchange exchange) {
Map<String, String> weights = exchange.getAttribute(WEIGHT_ATTR);
if (weights == null) {
weights = new ConcurrentHashMap<>();
exchange.getAttributes().put(WEIGHT_ATTR, weights);
}
return weights;
}
/* for testing */ static class GroupWeightConfig {
String group;
LinkedHashMap<String, Integer> weights = new LinkedHashMap<>();
LinkedHashMap<String, Double> normalizedWeights = new LinkedHashMap<>();
LinkedHashMap<Integer, String> rangeIndexes = new LinkedHashMap<>();
List<Double> ranges = new ArrayList<>();
GroupWeightConfig(String group) {
this.group = group;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("group", group)
.append("weights", weights)
.append("normalizedWeights", normalizedWeights)
.append("rangeIndexes", rangeIndexes)
.toString();
}
}
}

View File

@@ -26,6 +26,7 @@ import org.springframework.web.reactive.handler.AbstractHandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_HANDLER_MAPPER_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
import reactor.core.publisher.Mono;
@@ -52,6 +53,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
return lookupRoute(exchange)
// .log("route-predicate-handler-mapping", Level.FINER) //name this
.flatMap((Function<Route, Mono<?>>) r -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isDebugEnabled()) {
logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
}
@@ -59,6 +61,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
return Mono.just(webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
if (logger.isTraceEnabled()) {
logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
}
@@ -84,10 +87,13 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
return out.toString();
}
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
.filter(route -> route.getPredicate().test(exchange))
.filter(route -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId());
return route.getPredicate().test(exchange);
})
// .defaultIfEmpty() put a static Route not found
// or .switchIfEmpty()
// .switchIfEmpty(Mono.<Route>empty().log("noroute"))

View File

@@ -42,9 +42,12 @@ public interface RoutePredicateFactory<C> extends ShortcutConfigurable, Configur
default Predicate<ServerWebExchange> apply(Consumer<C> consumer) {
C config = newConfig();
consumer.accept(config);
beforeApply(config);
return apply(config);
}
default void beforeApply(C config) {}
//TODO: remove after apply(Tuple) removed
@Override
default Class<C> getConfigClass() {

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2013-2018 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
*
* http://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.gateway.handler.predicate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.event.WeightDefinedEvent;
import org.springframework.cloud.gateway.support.WeightConfig;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.web.server.ServerWebExchange;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.WEIGHT_ATTR;
/**
* @author Spencer Gibb
*/
//TODO: make this a generic Choose out of group predicate?
public class WeightRoutePredicateFactory extends AbstractRoutePredicateFactory<WeightConfig> implements ApplicationEventPublisherAware {
private static final Log log = LogFactory.getLog(WeightRoutePredicateFactory.class);
public static final String GROUP_KEY = WeightConfig.CONFIG_PREFIX + ".group";
public static final String WEIGHT_KEY = WeightConfig.CONFIG_PREFIX + ".weight";
private ApplicationEventPublisher publisher;
public WeightRoutePredicateFactory() {
super(WeightConfig.class);
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(GROUP_KEY, WEIGHT_KEY);
}
@Override
public String shortcutFieldPrefix() {
return WeightConfig.CONFIG_PREFIX;
}
@Override
public void beforeApply(WeightConfig config) {
if (publisher != null) {
publisher.publishEvent(new WeightDefinedEvent(this, config));
}
}
@Override
public Predicate<ServerWebExchange> apply(WeightConfig config) {
return exchange -> {
Map<String, String> weights = exchange.getAttributeOrDefault(WEIGHT_ATTR,
Collections.emptyMap());
String routeId = exchange.getAttribute(GATEWAY_PREDICATE_ROUTE_ATTR);
// all calculations and comparison against random num happened in
// WeightCalculatorWebFilter
String group = config.getGroup();
if (weights.containsKey(group)) {
String chosenRoute = weights.get(group);
if (log.isTraceEnabled()) {
log.trace("in group weight: "+ group + ", current route: " + routeId +", chosen route: " + chosenRoute);
}
return routeId.equals(chosenRoute);
}
return false;
};
}
}

View File

@@ -157,7 +157,7 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
Object configuration = factory.newConfig();
ConfigurationUtils.bind(configuration, properties,
"", definition.getName(), validator);
factory.shortcutFieldPrefix(), definition.getName(), validator);
GatewayFilter gatewayFilter = factory.apply(configuration);
if (this.publisher != null) {
@@ -262,7 +262,7 @@ public class RouteDefinitionRouteLocator implements RouteLocator, BeanFactoryAwa
Map<String, Object> properties = factory.shortcutType().normalize(args, factory, this.parser, this.beanFactory);
Object config = factory.newConfig();
ConfigurationUtils.bind(config, properties,
"", predicate.getName(), validator);
factory.shortcutFieldPrefix(), predicate.getName(), validator);
if (this.publisher != null) {
this.publisher.publishEvent(new PredicateArgsEvent(this, route.getId(), properties));
}

View File

@@ -47,8 +47,9 @@ public class BooleanSpec extends UriSpec {
return new BooleanOpSpec(routeBuilder, builder, OR);
}
public BooleanOpSpec negate() {
return new BooleanOpSpec(routeBuilder, builder, NEGATE);
public BooleanSpec negate() {
this.routeBuilder.negate();
return new BooleanSpec(routeBuilder, builder);
}
public UriSpec filters(Function<GatewayFilterSpec, UriSpec> fn) {

View File

@@ -29,6 +29,7 @@ import org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateF
import org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.http.HttpMethod;
import org.springframework.web.server.ServerWebExchange;
@@ -117,6 +118,13 @@ public class PredicateSpec extends UriSpec {
.apply(c -> c.setSources(addrs)));
}
public BooleanSpec weight(String group, int weight) {
return predicate(getBean(WeightRoutePredicateFactory.class)
.apply(c -> c.setGroup(group)
.setRouteId(routeBuilder.getId())
.setWeight(weight)));
}
public BooleanSpec alwaysTrue() {
return predicate(exchange -> true);
}

View File

@@ -41,6 +41,8 @@ public class ServerWebExchangeUtils {
public static final String GATEWAY_ORIGINAL_REQUEST_URL_ATTR = qualify("gatewayOriginalRequestUrl");
public static final String GATEWAY_HANDLER_MAPPER_ATTR = qualify("gatewayHandlerMapper");
public static final String GATEWAY_SCHEME_PREFIX_ATTR = qualify("gatewaySchemePrefix");
public static final String GATEWAY_PREDICATE_ROUTE_ATTR = qualify("gatewayPredicateRouteAttr");
public static final String WEIGHT_ATTR = qualify("routeWeight");
/**
* Used when a routing filter has been successfully call. Allows users to write custom

View File

@@ -115,6 +115,10 @@ public interface ShortcutConfigurable {
return Collections.emptyList();
}
default String shortcutFieldPrefix() {
return "";
}
/**
* Validate supplied argument size against {@see #shortcutFieldOrder} size.
* Useful for variable arg predicates.

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2013-2018 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
*
* http://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.gateway.support;
import org.springframework.core.style.ToStringCreator;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
@Validated
public class WeightConfig {
public static final String CONFIG_PREFIX = "weight";
@NotEmpty
private String group;
private String routeId;
@Min(0)
private int weight;
private WeightConfig() { }
public WeightConfig(String group, String routeId, int weight) {
this.routeId = routeId;
this.group = group;
this.weight = weight;
}
public WeightConfig(String routeId) {
this.routeId = routeId;
}
public String getGroup() {
return group;
}
public WeightConfig setGroup(String group) {
this.group = group;
return this;
}
public String getRouteId() {
return routeId;
}
public WeightConfig setRouteId(String routeId) {
this.routeId = routeId;
return this;
}
public int getWeight() {
return weight;
}
public WeightConfig setWeight(int weight) {
this.weight = weight;
return this;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("group", group)
.append("routeId", routeId)
.append("weight", weight)
.toString();
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2013-2018 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
*
* http://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.gateway.filter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.cloud.gateway.event.PredicateArgsEvent;
import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter.GroupWeightConfig;
import org.springframework.cloud.gateway.support.WeightConfig;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import static java.util.Arrays.asList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WeightCalculatorWebFilterTests {
@Test
public void testWeightCalculation() {
WeightCalculatorWebFilter filter = new WeightCalculatorWebFilter();
String grp1 = "group1";
String grp2 = "group2";
int grp1idx = 1;
int grp2idx = 1;
assertWeightCalculation(filter, grp1, grp1idx++, 1, asList(1.0));
assertWeightCalculation(filter, grp2, grp2idx++, 1, asList(1.0));
assertWeightCalculation(filter, grp1, grp1idx++, 3, asList(0.25, 0.75), 0.25);
assertWeightCalculation(filter, grp2, grp2idx++, 1, asList(0.5, 0.5), 0.5);
assertWeightCalculation(filter, grp1, grp1idx++, 6, asList(0.1, 0.3, 0.6), 0.1, 0.4);
assertWeightCalculation(filter, grp2, grp2idx++, 2, asList(0.25, 0.25, 0.5), 0.25, 0.5);
assertWeightCalculation(filter, grp2, grp2idx++, 4, asList(0.125, 0.125, 0.25, 0.5), 0.125, 0.25, 0.5);
}
private void assertWeightCalculation(WeightCalculatorWebFilter filter, String group, int item,
int weight, List<Double> normalized, Double... middleRanges) {
String routeId = route(item);
filter.addWeightConfig(new WeightConfig(group, routeId, weight));
Map<String, GroupWeightConfig> groupWeights = filter.getGroupWeights();
assertThat(groupWeights).containsKey(group);
GroupWeightConfig config = groupWeights.get(group);
assertThat(config.group).isEqualTo(group);
assertThat(config.weights).hasSize(item)
.containsEntry(routeId, weight);
assertThat(config.normalizedWeights).hasSize(item);
for (int i = 0; i < normalized.size(); i++) {
assertThat(config.normalizedWeights)
.containsEntry(route(i+1), normalized.get(i));
}
for (int i = 0; i < normalized.size(); i++) {
assertThat(config.rangeIndexes)
.containsEntry(i, route(i+1));
}
assertThat(config.ranges).hasSize(item + 1)
.startsWith(0.0)
.endsWith(1.0);
if (middleRanges.length > 0) {
assertThat(config.ranges).contains(middleRanges);
}
}
@NotNull
private String route(int i) {
return "route"+i;
}
@Test
public void testChooseRouteWithRandom() {
WeightCalculatorWebFilter filter = new WeightCalculatorWebFilter();
filter.addWeightConfig(new WeightConfig("groupa", "route1", 1));
filter.addWeightConfig(new WeightConfig("groupa", "route2", 3));
filter.addWeightConfig(new WeightConfig("groupa", "route3", 6));
Random random = mock(Random.class);
when(random.nextDouble())
.thenReturn(0.05)
.thenReturn(0.2)
.thenReturn(0.6);
filter.setRandom(random);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost").build());
WebFilterChain filterChain = mock(WebFilterChain.class);
filter.filter(exchange, filterChain);
Map<String, String> weights = WeightCalculatorWebFilter.getWeights(exchange);
assertThat(weights).containsEntry("groupa", "route1");
filter.filter(exchange, filterChain);
weights = WeightCalculatorWebFilter.getWeights(exchange);
assertThat(weights).containsEntry("groupa", "route2");
filter.filter(exchange, filterChain);
weights = WeightCalculatorWebFilter.getWeights(exchange);
assertThat(weights).containsEntry("groupa", "route3");
}
@Test
public void receivesPredicateArgsEvent() {
WeightCalculatorWebFilter filter = mock(WeightCalculatorWebFilter.class);
doNothing().when(filter).addWeightConfig(any(WeightConfig.class));
doCallRealMethod().when(filter).handle(any(PredicateArgsEvent.class));
HashMap<String, Object> args = new HashMap<>();
args.put("weight.group", "group1");
args.put("weight.weight", "1");
PredicateArgsEvent event = new PredicateArgsEvent(this, "routeA", args);
filter.handle(event);
ArgumentCaptor<WeightConfig> configCaptor = ArgumentCaptor.forClass(WeightConfig.class);
verify(filter).addWeightConfig(configCaptor.capture());
WeightConfig weightConfig = configCaptor.getValue();
assertThat(weightConfig.getGroup()).isEqualTo("group1");
assertThat(weightConfig.getRouteId()).isEqualTo("routeA");
assertThat(weightConfig.getWeight()).isEqualTo(1);
}
}

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2013-2018 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
*
* http://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.gateway.handler.predicate;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
public class WeightRoutePredicateFactoryIntegrationTests extends BaseWebClientTests {
@Autowired
private WeightCalculatorWebFilter filter;
@Test
public void highWeight() {
filter.setRandom(getRandom(0.9));
testClient.get().uri("/get")
.header(HttpHeaders.HOST, "www.weighthigh.org")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(ROUTE_ID_HEADER, "weight_high_test");
}
@Test
public void lowWeight() {
filter.setRandom(getRandom(0.1));
testClient.get().uri("/get")
.header(HttpHeaders.HOST, "www.weightlow.org")
.exchange()
.expectStatus().isOk()
.expectHeader().valueEquals(ROUTE_ID_HEADER, "weight_low_test");
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig {
@Value("${test.uri}")
private String uri;
public TestConfig(WeightCalculatorWebFilter filter) {
Random random = getRandom(0.4);
filter.setRandom(random);
}
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("weight_low_test", r ->
r.weight("group1", 2)
.and().host("**.weightlow.org")
.filters(f -> f.prefixPath("/httpbin"))
.uri(this.uri))
.build();
}
}
private static Random getRandom(double value) {
Random random = mock(Random.class);
when(random.nextDouble())
.thenReturn(value);
return random;
}
}

View File

@@ -222,11 +222,19 @@ spring:
- AddRequestHeader=X-Request-Foo, Bar
- AddRequestHeader=X-Request-Baz, Bat
# =====================================
- id: weight_high_test
uri: ${test.uri}
predicates:
- Host=**.weighthigh.org
- Weight=group1, 8
# =====================================
- id: header_test
uri: ${test.uri}
predicates:
- Header=Foo, .*
# =====================================
- id: default_path_to_httpbin
uri: ${test.uri}