diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index b0be738b..4f6fefa3 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -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 diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/WeightDefinedEvent.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/WeightDefinedEvent.java new file mode 100644 index 00000000..b1c5cccb --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/WeightDefinedEvent.java @@ -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; + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WeightCalculatorWebFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WeightCalculatorWebFilter.java new file mode 100644 index 00000000..0868e438 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WeightCalculatorWebFilter.java @@ -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 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 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 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 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 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 getGroupWeights() { + return groupWeights; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + Map weights = getWeights(exchange); + + groupWeights.forEach((group, config) -> { + double r = this.random.nextDouble(); + + List 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 getWeights(ServerWebExchange exchange) { + Map 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 weights = new LinkedHashMap<>(); + + LinkedHashMap normalizedWeights = new LinkedHashMap<>(); + + LinkedHashMap rangeIndexes = new LinkedHashMap<>(); + List 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(); + } + } + +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java index b09b11dc..b10a5d2f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java @@ -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>) 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 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.empty().log("noroute")) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/RoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/RoutePredicateFactory.java index 54eee0f1..6afd85fa 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/RoutePredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/RoutePredicateFactory.java @@ -42,9 +42,12 @@ public interface RoutePredicateFactory extends ShortcutConfigurable, Configur default Predicate apply(Consumer 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 getConfigClass() { diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/WeightRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/WeightRoutePredicateFactory.java new file mode 100644 index 00000000..5d41d33c --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/WeightRoutePredicateFactory.java @@ -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 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 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 apply(WeightConfig config) { + return exchange -> { + Map 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; + }; + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.java index 0710f483..1bb910fb 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.java @@ -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 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)); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java index d23bb182..8cf00aa8 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/BooleanSpec.java @@ -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 fn) { diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java index e404196e..59fc96cd 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java @@ -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); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java index 4f8d2e18..2f9b48d3 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ServerWebExchangeUtils.java @@ -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 diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java index 3f3d7677..6f172d38 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java @@ -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. diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/WeightConfig.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/WeightConfig.java new file mode 100644 index 00000000..05bbaa33 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/WeightConfig.java @@ -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(); + } +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WeightCalculatorWebFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WeightCalculatorWebFilterTests.java new file mode 100644 index 00000000..ec7b7515 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WeightCalculatorWebFilterTests.java @@ -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 normalized, Double... middleRanges) { + String routeId = route(item); + + filter.addWeightConfig(new WeightConfig(group, routeId, weight)); + + Map 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 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 args = new HashMap<>(); + args.put("weight.group", "group1"); + args.put("weight.weight", "1"); + PredicateArgsEvent event = new PredicateArgsEvent(this, "routeA", args); + filter.handle(event); + + ArgumentCaptor 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); + } +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/WeightRoutePredicateFactoryIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/WeightRoutePredicateFactoryIntegrationTests.java new file mode 100644 index 00000000..ca38051c --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/WeightRoutePredicateFactoryIntegrationTests.java @@ -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; + } + +} diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index a140fb37..c003a8b4 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -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}