Adds support for RSocket composite metadata.

Renames Registry to RoutingTable implemented as an inverted
index as TagsMetadata. Inverted index uses RoaringBitmaps to save
memory and to make lookup very fast.

Implements RoutingTableRoutes as a view over RoutingTable.

Route is now an interface. The Route objects returned by
RoutingTableRoutes are backed by a predicate that uses the RoutingTable
rather than custom matching.

GatewayRSockectFactory moved to a top level class.

PendingRequestRSocket also has a top level Factory.

Load balancing is now handled by a Factory. Allows requests to ask
for specific types of load balancers in the future.

Implements proposed RouteSetup and Forwarding metadata types in
the Gateway. These should eventually be replaced by primitives in
rsocket-java. Also adds support to be encoded/decoded using
Frameworks Encoder/Decoder classes.

fixes gh-1236
This commit is contained in:
Spencer Gibb
2019-08-13 16:29:25 -05:00
parent 6687594212
commit 1c4a8518bc
43 changed files with 3188 additions and 1099 deletions

11
pom.xml
View File

@@ -52,7 +52,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<rsocket.version>1.0.0-RC2</rsocket.version>
<rsocket.version>1.0.0-RC3-SNAPSHOT</rsocket.version>
<spring-cloud-commons.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-netflix.version>2.2.0.BUILD-SNAPSHOT</spring-cloud-netflix.version>
<embedded-redis.version>0.6</embedded-redis.version>
@@ -212,6 +212,15 @@
<enabled>false</enabled>
</snapshots>
</repository>
<!-- RSocket Snapshots -->
<repository>
<id>jfrog-snapshots</id>
<name>JFRog Snapshots</name>
<url>https://oss.jfrog.org/artifactory/libs-snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>

View File

@@ -18,6 +18,7 @@
<description>Spring Cloud Gateway Dependencies</description>
<properties>
<roaringbitmap.version>0.8.9</roaringbitmap.version>
</properties>
<dependencyManagement>
@@ -47,6 +48,11 @@
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
<version>${roaringbitmap.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -56,6 +56,10 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>org.roaringbitmap</groupId>
<artifactId>RoaringBitmap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>

View File

@@ -26,21 +26,32 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.rsocket.messaging.RSocketStrategiesCustomizer;
import org.springframework.boot.rsocket.server.RSocketServerBootstrap;
import org.springframework.boot.rsocket.server.RSocketServerFactory;
import org.springframework.cloud.gateway.rsocket.core.GatewayRSocket;
import org.springframework.cloud.gateway.rsocket.core.GatewayRSocketFactory;
import org.springframework.cloud.gateway.rsocket.core.GatewayServerRSocketFactoryCustomizer;
import org.springframework.cloud.gateway.rsocket.registry.Registry;
import org.springframework.cloud.gateway.rsocket.registry.RegistryRoutes;
import org.springframework.cloud.gateway.rsocket.core.PendingRequestRSocketFactory;
import org.springframework.cloud.gateway.rsocket.registry.LoadBalancerFactory;
import org.springframework.cloud.gateway.rsocket.registry.RegistrySocketAcceptorFilter;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTable;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTableRoutes;
import org.springframework.cloud.gateway.rsocket.route.Routes;
import org.springframework.cloud.gateway.rsocket.socketacceptor.GatewaySocketAcceptor;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilter;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorPredicate;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorPredicateFilter;
import org.springframework.cloud.gateway.rsocket.support.Forwarding;
import org.springframework.cloud.gateway.rsocket.support.RouteSetup;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.messaging.rsocket.DefaultMetadataExtractor;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.messaging.rsocket.RSocketStrategies;
import static org.springframework.cloud.gateway.rsocket.support.Forwarding.FORWARDING_MIME_TYPE;
import static org.springframework.cloud.gateway.rsocket.support.RouteSetup.ROUTE_SETUP_MIME_TYPE;
/**
* @author Spencer Gibb
@@ -54,27 +65,43 @@ import org.springframework.core.env.Environment;
public class GatewayRSocketAutoConfiguration {
@Bean
public Registry registry() {
return new Registry();
public RoutingTable routingTable() {
return new RoutingTable();
}
// TODO: CompositeRoutes
@Bean
public RegistryRoutes registryRoutes(Registry registry) {
RegistryRoutes registryRoutes = new RegistryRoutes();
registry.addListener(registryRoutes);
return registryRoutes;
public RoutingTableRoutes registryRoutes(RoutingTable routingTable) {
return new RoutingTableRoutes(routingTable);
}
@Bean
public RegistrySocketAcceptorFilter registrySocketAcceptorFilter(Registry registry) {
return new RegistrySocketAcceptorFilter(registry);
public RegistrySocketAcceptorFilter registrySocketAcceptorFilter(
RoutingTable routingTable) {
return new RegistrySocketAcceptorFilter(routingTable);
}
@Bean
public GatewayRSocket.Factory gatewayRSocketFactory(Registry registry, Routes routes,
MeterRegistry meterRegistry, GatewayRSocketProperties properties) {
return new GatewayRSocket.Factory(registry, routes, meterRegistry, properties);
public PendingRequestRSocketFactory pendingRequestRSocketFactory(
RoutingTable routingTable, Routes routes,
RSocketStrategies rSocketStrategies) {
return new PendingRequestRSocketFactory(routingTable, routes,
rSocketStrategies.metadataExtractor());
}
@Bean
public LoadBalancerFactory loadBalancerFactory(RoutingTable routingTable) {
return new LoadBalancerFactory(routingTable);
}
@Bean
public GatewayRSocketFactory gatewayRSocketFactory(RoutingTable routingTable,
Routes routes, PendingRequestRSocketFactory pendingFactory,
LoadBalancerFactory loadBalancerFactory, MeterRegistry meterRegistry,
GatewayRSocketProperties properties, RSocketStrategies rSocketStrategies) {
return new GatewayRSocketFactory(routingTable, routes, pendingFactory,
loadBalancerFactory, meterRegistry, properties,
rSocketStrategies.metadataExtractor());
}
@Bean
@@ -94,11 +121,26 @@ public class GatewayRSocketAutoConfiguration {
}
@Bean
public GatewaySocketAcceptor socketAcceptor(GatewayRSocket.Factory rsocketFactory,
public GatewaySocketAcceptor socketAcceptor(GatewayRSocketFactory rsocketFactory,
List<SocketAcceptorFilter> filters, MeterRegistry meterRegistry,
GatewayRSocketProperties properties) {
GatewayRSocketProperties properties, RSocketStrategies rSocketStrategies) {
MetadataExtractor metadataExtractor = registerMimeTypes(rSocketStrategies);
return new GatewaySocketAcceptor(rsocketFactory, filters, meterRegistry,
properties);
properties, metadataExtractor);
}
public static MetadataExtractor registerMimeTypes(
RSocketStrategies rSocketStrategies) {
MetadataExtractor metadataExtractor = rSocketStrategies.metadataExtractor();
// TODO: see if possible to make easier in framework.
if (metadataExtractor instanceof DefaultMetadataExtractor) {
DefaultMetadataExtractor extractor = (DefaultMetadataExtractor) metadataExtractor;
extractor.metadataToExtract(FORWARDING_MIME_TYPE, Forwarding.class,
"forwarding");
extractor.metadataToExtract(ROUTE_SETUP_MIME_TYPE, RouteSetup.class,
"routesetup");
}
return metadataExtractor;
}
@Bean
@@ -114,4 +156,12 @@ public class GatewayRSocketAutoConfiguration {
return new RSocketServerBootstrap(rSocketServerFactory, gatewaySocketAcceptor);
}
@Bean
public RSocketStrategiesCustomizer gatewayRSocketStrategiesCustomizer() {
return strategies -> {
strategies.decoder(new Forwarding.Decoder(), new RouteSetup.Decoder())
.encoder(new Forwarding.Encoder(), new RouteSetup.Encoder());
};
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.core;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.rsocket.AbstractRSocket;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.ResponderRSocket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.rsocket.autoconfigure.GatewayRSocketProperties;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Convience class to hold and calculate exchange and metrics related information.
*/
public abstract class AbstractGatewayRSocket extends AbstractRSocket
implements ResponderRSocket {
private static final Log log = LogFactory.getLog(AbstractGatewayRSocket.class);
protected final MeterRegistry meterRegistry;
private final GatewayRSocketProperties properties;
private final MetadataExtractor metadataExtractor;
private final TagsMetadata metadata;
AbstractGatewayRSocket(MeterRegistry meterRegistry,
GatewayRSocketProperties properties, MetadataExtractor metadataExtractor,
TagsMetadata metadata) {
this.meterRegistry = meterRegistry;
this.properties = properties;
this.metadataExtractor = metadataExtractor;
this.metadata = metadata;
}
protected GatewayExchange createExchange(GatewayExchange.Type type, Payload payload) {
GatewayExchange exchange = GatewayExchange.fromPayload(type, payload,
metadataExtractor);
Tags tags = getTags(exchange);
exchange.setTags(tags);
return exchange;
}
protected Tags getTags(GatewayExchange exchange) {
// TODO: add tags to exchange
String requesterName = "FIXME"; // FIXME: this.metadata.get(SERVICE_NAME);
String requesterId = "FIXME"; // FIXME: this.metadata.getRouteId();
String responderName = "FIXME"; // FIXME: exchange.getRoutingMetadata().getName();
Assert.hasText(responderName, "responderName must not be empty");
Assert.hasText(requesterId, "requesterId must not be empty");
Assert.hasText(requesterName, "requesterName must not be empty");
// responder.id happens in a callback, later
return Tags.of("requester.name", requesterName, "responder.name", responderName,
"requester.id", requesterId, "gateway.id", this.properties.getId());
}
protected void count(GatewayExchange exchange, String suffix) {
count(exchange, suffix, Tags.empty());
}
protected void count(GatewayExchange exchange, Tags additionalTags) {
count(exchange, null, additionalTags);
}
protected void count(GatewayExchange exchange, String suffix, Tags additionalTags) {
Tags tags = exchange.getTags().and(additionalTags);
String name = getMetricName(exchange, suffix);
this.meterRegistry.counter(name, tags).increment();
}
protected String getMetricName(GatewayExchange exchange) {
return getMetricName(exchange, null);
}
protected String getMetricName(GatewayExchange exchange, String suffix) {
StringBuilder name = new StringBuilder("forward.");
name.append(exchange.getType().getKey());
if (StringUtils.hasLength(suffix)) {
name.append(".");
name.append(suffix);
}
return name.toString();
}
protected Mono<RSocket> doOnEmpty(GatewayExchange exchange) {
if (log.isDebugEnabled()) {
log.debug("Unable to find destination RSocket for "
+ exchange.getRoutingMetadata());
}
return Mono.empty();
}
}

View File

@@ -16,13 +16,17 @@
package org.springframework.cloud.gateway.rsocket.core;
import java.util.Map;
import io.micrometer.core.instrument.Tags;
import io.rsocket.Payload;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.rsocket.filter.AbstractRSocketExchange;
import org.springframework.cloud.gateway.rsocket.support.Forwarding;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.messaging.rsocket.MetadataExtractor;
/**
* Exchange object used in GatewayFilterChain started by GatewayRSocket.
@@ -36,11 +40,27 @@ public class GatewayExchange extends AbstractRSocketExchange {
*/
public static final String ROUTE_ATTR = "__route_attr_";
enum Type {
public enum Type {
FIRE_AND_FORGET("request.fnf"), REQUEST_CHANNEL(
"request.channel"), REQUEST_RESPONSE(
"request.response"), REQUEST_STREAM("request.stream");
/**
* RSocket fire and forget request type.
*/
FIRE_AND_FORGET("request.fnf"),
/**
* RSocket request channel request type.
*/
REQUEST_CHANNEL("request.channel"),
/**
* RSocket request response request type.
*/
REQUEST_RESPONSE("request.response"),
/**
* RSocket request stream request type.
*/
REQUEST_STREAM("request.stream");
private String key;
@@ -56,30 +76,38 @@ public class GatewayExchange extends AbstractRSocketExchange {
private final Type type;
private final Metadata routingMetadata;
private final Forwarding routingMetadata;
private Tags tags = Tags.empty();
public static GatewayExchange fromPayload(Type type, Payload payload) {
return new GatewayExchange(type, getRoutingMetadata(payload));
public static GatewayExchange fromPayload(Type type, Payload payload,
MetadataExtractor metadataExtractor) {
return new GatewayExchange(type, getRoutingMetadata(metadataExtractor, payload));
}
private static Metadata getRoutingMetadata(Payload payload) {
private static Forwarding getRoutingMetadata(MetadataExtractor metadataExtractor,
Payload payload) {
if (payload == null || !payload.hasMetadata()) { // and metadata is routing
return null;
}
// TODO: deal with composite metadata
// TODO: deal with payload mimetype
Map<String, Object> metadataMap = metadataExtractor.extract(payload,
Metadata.COMPOSITE_MIME_TYPE);
Metadata metadata = Metadata.decodeMetadata(payload.sliceMetadata());
if (metadataMap.containsKey("forwarding")) {
Forwarding metadata = (Forwarding) metadataMap.get("forwarding");
if (log.isDebugEnabled()) {
log.debug("found routing metadata " + metadata);
if (log.isDebugEnabled()) {
log.debug("found routing metadata " + metadata);
}
return metadata;
}
return metadata;
return null;
}
public GatewayExchange(Type type, Metadata routingMetadata) {
public GatewayExchange(Type type, Forwarding routingMetadata) {
this.type = type;
this.routingMetadata = routingMetadata;
}
@@ -88,7 +116,7 @@ public class GatewayExchange extends AbstractRSocketExchange {
return type;
}
public Metadata getRoutingMetadata() {
public Forwarding getRoutingMetadata() {
return routingMetadata;
}

View File

@@ -17,31 +17,26 @@
package org.springframework.cloud.gateway.rsocket.core;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.logging.Level;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.Timer;
import io.rsocket.AbstractRSocket;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.ResponderRSocket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Publisher;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.rsocket.autoconfigure.GatewayRSocketProperties;
import org.springframework.cloud.gateway.rsocket.registry.LoadBalancedRSocket;
import org.springframework.cloud.gateway.rsocket.registry.Registry;
import org.springframework.cloud.gateway.rsocket.registry.LoadBalancerFactory;
import org.springframework.cloud.gateway.rsocket.route.Route;
import org.springframework.cloud.gateway.rsocket.route.Routes;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.messaging.rsocket.MetadataExtractor;
import static org.springframework.cloud.gateway.rsocket.core.GatewayExchange.ROUTE_ATTR;
import static org.springframework.cloud.gateway.rsocket.core.GatewayExchange.Type.FIRE_AND_FORGET;
@@ -57,46 +52,28 @@ import static org.springframework.cloud.gateway.rsocket.core.GatewayFilterChain.
* to locate a target RSocket via the Registry is executed. If not found a pending RSocket
* * is returned.
*/
public class GatewayRSocket extends AbstractRSocket implements ResponderRSocket {
public class GatewayRSocket extends AbstractGatewayRSocket {
private static final Log log = LogFactory.getLog(GatewayRSocket.class);
private final Registry registry;
private final Routes routes;
private final MeterRegistry meterRegistry;
private final PendingRequestRSocketFactory pendingFactory;
private final GatewayRSocketProperties properties;
private final LoadBalancerFactory loadBalancerFactory;
private final Metadata metadata;
GatewayRSocket(Registry registry, Routes routes, MeterRegistry meterRegistry,
GatewayRSocketProperties properties, Metadata metadata) {
this.registry = registry;
GatewayRSocket(Routes routes, PendingRequestRSocketFactory pendingFactory,
LoadBalancerFactory loadBalancerFactory, MeterRegistry meterRegistry,
GatewayRSocketProperties properties, MetadataExtractor metadataExtractor,
TagsMetadata metadata) {
super(meterRegistry, properties, metadataExtractor, metadata);
this.routes = routes;
this.meterRegistry = meterRegistry;
this.properties = properties;
this.metadata = metadata;
this.onClose().doOnSuccess(v -> registry.deregister(metadata))
// .doOnNext(v -> log.error("OnClose doOnNext"))
.doOnError(t -> {
if (log.isErrorEnabled()) {
log.error("Error received, deregistering " + metadata, t);
}
registry.deregister(metadata);
})
// .doOnTerminate(() -> log.error("OnClose doOnTerminate"))
// .doFinally(st -> log.error("OnClose doFinally"))
.subscribe();
this.pendingFactory = pendingFactory;
this.loadBalancerFactory = loadBalancerFactory;
}
protected Registry getRegistry() {
return registry;
}
protected Routes getRoutes() {
return routes;
protected PendingRequestRSocketFactory getPendingFactory() {
return this.pendingFactory;
}
@Override
@@ -108,26 +85,6 @@ public class GatewayRSocket extends AbstractRSocket implements ResponderRSocket
.doFinally(s -> count(exchange, ""));
}
private GatewayExchange createExchange(GatewayExchange.Type type, Payload payload) {
GatewayExchange exchange = GatewayExchange.fromPayload(type, payload);
Tags tags = getTags(exchange);
exchange.setTags(tags);
return exchange;
}
private Tags getTags(GatewayExchange exchange) {
// TODO: add tags to exchange
String requesterName = this.metadata.getName();
String requesterId = this.metadata.get("id");
String responderName = exchange.getRoutingMetadata().getName();
Assert.hasText(responderName, "responderName must not be empty");
Assert.hasText(requesterId, "requesterId must not be empty");
Assert.hasText(requesterName, "requesterName must not be empty");
// responder.id happens in a callback, later
return Tags.of("requester.name", requesterName, "responder.name", responderName,
"requester.id", requesterId, "gateway.id", this.properties.getId());
}
@Override
public Flux<Payload> requestChannel(Payload payload, Publisher<Payload> payloads) {
GatewayExchange exchange = createExchange(REQUEST_CHANNEL, payload);
@@ -151,34 +108,6 @@ public class GatewayRSocket extends AbstractRSocket implements ResponderRSocket
.doFinally(s -> count(exchange, responderTags));
}
private void count(GatewayExchange exchange, String suffix) {
count(exchange, suffix, Tags.empty());
}
private void count(GatewayExchange exchange, Tags additionalTags) {
count(exchange, null, additionalTags);
}
private void count(GatewayExchange exchange, String suffix, Tags additionalTags) {
Tags tags = exchange.getTags().and(additionalTags);
String name = getMetricName(exchange, suffix);
this.meterRegistry.counter(name, tags).increment();
}
private String getMetricName(GatewayExchange exchange) {
return getMetricName(exchange, null);
}
private String getMetricName(GatewayExchange exchange, String suffix) {
StringBuilder name = new StringBuilder("forward.");
name.append(exchange.getType().getKey());
if (StringUtils.hasLength(suffix)) {
name.append(".");
name.append(suffix);
}
return name.toString();
}
@Override
public Mono<Payload> requestResponse(Payload payload) {
AtomicReference<Timer.Sample> timer = new AtomicReference<>();
@@ -211,56 +140,7 @@ public class GatewayRSocket extends AbstractRSocket implements ResponderRSocket
private Mono<RSocket> findRSocketOrCreatePending(GatewayExchange exchange) {
return findRSocket(exchange)
// if a route can't be found or registered RSocket, create pending
.switchIfEmpty(createPendingRSocket(exchange));
}
private Mono<RSocket> createPendingRSocket(GatewayExchange exchange) {
if (log.isDebugEnabled()) {
log.debug("creating pending RSocket for " + exchange.getRoutingMetadata());
}
PendingRequestRSocket pending = constructPendingRSocket(exchange);
Disposable disposable = this.registry.addListener(pending);
pending.setSubscriptionDisposable(disposable);
return Mono.just(pending);
}
/* for testing */ PendingRequestRSocket constructPendingRSocket(
GatewayExchange exchange) {
Function<Registry.RegisteredEvent, Mono<Route>> routeFinder = registeredEvent -> getRouteMono(
registeredEvent, exchange);
return new PendingRequestRSocket(routeFinder, map -> {
Tags tags = exchange.getTags().and("responder.id", map.get("id"));
exchange.setTags(tags);
});
}
protected Mono<Route> getRouteMono(Registry.RegisteredEvent registeredEvent,
GatewayExchange exchange) {
return findRoute(exchange)
.log(PendingRequestRSocket.class.getName() + ".find route pending",
Level.FINEST)
// can this be replaced with filter?
.flatMap(
route -> matchRoute(route, registeredEvent.getRoutingMetadata()));
}
private Mono<Route> findRoute(GatewayExchange exchange) {
Mono<Route> routeMono;
/*
* if (this.route != null) { //TODO: cache Route? routeMono = Mono.just(route); }
* else {
*/
routeMono = this.routes.findRoute(exchange);
// }
return routeMono;
}
private Mono<Route> matchRoute(Route route, Metadata annoucementMetadata) {
Metadata targetMetadata = route.getTargetMetadata();
if (targetMetadata.matches(annoucementMetadata)) {
return Mono.just(route);
}
return Mono.empty();
.switchIfEmpty(pendingFactory.create(exchange));
}
/**
@@ -275,55 +155,21 @@ public class GatewayRSocket extends AbstractRSocket implements ResponderRSocket
.flatMap(route -> {
// put route in exchange for later use
exchange.getAttributes().put(ROUTE_ATTR, route);
return executeFilterChain(route.getFilters(), exchange)
.flatMap(success -> {
LoadBalancedRSocket loadBalancedRSocket = registry
.getRegistered(exchange.getRoutingMetadata());
return loadBalancedRSocket.choose();
}).map(enrichedRSocket -> {
Metadata metadata = enrichedRSocket.getMetadata();
Tags tags = exchange.getTags().and("responder.id",
metadata.get("id"));
exchange.setTags(tags);
return enrichedRSocket;
}).cast(RSocket.class).switchIfEmpty(doOnEmpty(exchange));
return findRSocket(exchange, route);
});
// TODO: deal with connecting to cluster?
}
private Mono<RSocket> doOnEmpty(GatewayExchange exchange) {
if (log.isDebugEnabled()) {
log.debug("Unable to find destination RSocket for "
+ exchange.getRoutingMetadata());
}
return Mono.empty();
}
public static class Factory {
private final Registry registry;
private final Routes routes;
private final MeterRegistry meterRegistry;
private final GatewayRSocketProperties properties;
public Factory(Registry registry, Routes routes, MeterRegistry meterRegistry,
GatewayRSocketProperties properties) {
this.registry = registry;
this.routes = routes;
this.meterRegistry = meterRegistry;
this.properties = properties;
}
public GatewayRSocket create(Metadata metadata) {
return new GatewayRSocket(this.registry, this.routes, this.meterRegistry,
this.properties, metadata);
}
private Mono<RSocket> findRSocket(GatewayExchange exchange, Route route) {
return executeFilterChain(route.getFilters(), exchange).flatMap(
success -> loadBalancerFactory.choose(exchange.getRoutingMetadata()))
.map(tuple -> {
// TODO: this is routeId, should it be service name?
Tags tags = exchange.getTags().and("responder.id", tuple.getT1());
exchange.setTags(tags);
return tuple.getT2();
}).cast(RSocket.class).switchIfEmpty(doOnEmpty(exchange));
}
}

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.core;
import io.micrometer.core.instrument.MeterRegistry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.rsocket.autoconfigure.GatewayRSocketProperties;
import org.springframework.cloud.gateway.rsocket.registry.LoadBalancerFactory;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTable;
import org.springframework.cloud.gateway.rsocket.route.Routes;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.util.Assert;
import static org.springframework.cloud.gateway.rsocket.support.WellKnownKey.ROUTE_ID;
import static org.springframework.cloud.gateway.rsocket.support.WellKnownKey.SERVICE_NAME;
public class GatewayRSocketFactory {
private static final Log log = LogFactory.getLog(GatewayRSocket.class);
private final RoutingTable routingTable;
private final Routes routes;
private final PendingRequestRSocketFactory pendingFactory;
private final LoadBalancerFactory loadBalancerFactory;
private final MeterRegistry meterRegistry;
private final GatewayRSocketProperties properties;
private final MetadataExtractor metadataExtractor;
public GatewayRSocketFactory(RoutingTable routingTable, Routes routes,
PendingRequestRSocketFactory pendingFactory,
LoadBalancerFactory loadBalancerFactory, MeterRegistry meterRegistry,
GatewayRSocketProperties properties, MetadataExtractor metadataExtractor) {
this.routingTable = routingTable;
this.routes = routes;
this.pendingFactory = pendingFactory;
this.loadBalancerFactory = loadBalancerFactory;
this.meterRegistry = meterRegistry;
this.properties = properties;
this.metadataExtractor = metadataExtractor;
}
public GatewayRSocket create(TagsMetadata metadata) {
Assert.hasText(metadata.get(ROUTE_ID), "metadata must contain " + ROUTE_ID);
Assert.hasText(metadata.get(SERVICE_NAME),
"metadata must contain " + SERVICE_NAME);
GatewayRSocket gatewayRSocket = new GatewayRSocket(this.routes,
this.pendingFactory, this.loadBalancerFactory, this.meterRegistry,
this.properties, this.metadataExtractor, metadata);
gatewayRSocket.onClose().doOnSuccess(v -> {
if (log.isDebugEnabled()) {
log.debug("Closed, deregistering " + metadata);
}
routingTable.deregister(metadata);
}).doOnError(t -> {
if (log.isErrorEnabled()) {
log.error("Error received, deregistering " + metadata, t);
}
routingTable.deregister(metadata);
});
// .doOnNext(v -> log.error("OnClose doOnNext"))
// .doOnTerminate(() -> log.error("OnClose doOnTerminate"))
// .doFinally(st -> log.error("OnClose doFinally")).subscribe();
return gatewayRSocket;
}
}

View File

@@ -34,9 +34,10 @@ import reactor.core.publisher.MonoProcessor;
import reactor.util.function.Tuple2;
import org.springframework.cloud.gateway.rsocket.filter.RSocketFilter.Success;
import org.springframework.cloud.gateway.rsocket.registry.Registry.RegisteredEvent;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTable.RegisteredEvent;
import org.springframework.cloud.gateway.rsocket.route.Route;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.messaging.rsocket.MetadataExtractor;
import static org.springframework.cloud.gateway.rsocket.core.GatewayExchange.ROUTE_ATTR;
import static org.springframework.cloud.gateway.rsocket.core.GatewayExchange.Type.REQUEST_STREAM;
@@ -47,9 +48,12 @@ public class PendingRequestRSocket extends AbstractRSocket
private static final Log log = LogFactory.getLog(PendingRequestRSocket.class);
// TODO: if this were just routeId & route wasn't an exchange attr would be simpler.
private final Function<RegisteredEvent, Mono<Route>> routeFinder;
private final Consumer<Metadata> metadataCallback;
private final MetadataExtractor metadataExtractor;
private final Consumer<TagsMetadata> metadataCallback;
private final MonoProcessor<RSocket> rSocketProcessor;
@@ -57,16 +61,18 @@ public class PendingRequestRSocket extends AbstractRSocket
private Route route;
public PendingRequestRSocket(Function<RegisteredEvent, Mono<Route>> routeFinder,
Consumer<Metadata> metadataCallback) {
this(routeFinder, metadataCallback, MonoProcessor.create());
public PendingRequestRSocket(MetadataExtractor metadataExtractor,
Function<RegisteredEvent, Mono<Route>> routeFinder,
Consumer<TagsMetadata> metadataCallback) {
this(metadataExtractor, routeFinder, metadataCallback, MonoProcessor.create());
}
/* for testing */ PendingRequestRSocket(
/* for testing */ PendingRequestRSocket(MetadataExtractor metadataExtractor,
Function<RegisteredEvent, Mono<Route>> routeFinder,
Consumer<Metadata> metadataCallback,
Consumer<TagsMetadata> metadataCallback,
MonoProcessor<RSocket> rSocketProcessor) {
this.routeFinder = routeFinder;
this.metadataExtractor = metadataExtractor;
this.metadataCallback = metadataCallback;
this.rSocketProcessor = rSocketProcessor;
}
@@ -136,7 +142,7 @@ public class PendingRequestRSocket extends AbstractRSocket
Level.FINEST)
.flatMap(rSocket -> {
GatewayExchange exchange = GatewayExchange.fromPayload(REQUEST_STREAM,
payload);
payload, metadataExtractor);
exchange.getAttributes().put(ROUTE_ATTR, route);
// exchange.getAttributes().putAll(pendingExchange.getAttributes());
return Mono.just(rSocket)

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.core;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.logging.Level;
import io.micrometer.core.instrument.Tags;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.Disposable;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTable;
import org.springframework.cloud.gateway.rsocket.route.Route;
import org.springframework.cloud.gateway.rsocket.route.Routes;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.messaging.rsocket.MetadataExtractor;
public class PendingRequestRSocketFactory {
private static final Log log = LogFactory.getLog(PendingRequestRSocket.class);
private final RoutingTable routingTable;
private final Routes routes;
private final MetadataExtractor metadataExtractor;
public PendingRequestRSocketFactory(RoutingTable routingTable, Routes routes,
MetadataExtractor metadataExtractor) {
this.routingTable = routingTable;
this.routes = routes;
this.metadataExtractor = metadataExtractor;
}
public Mono<PendingRequestRSocket> create(GatewayExchange exchange) {
if (log.isDebugEnabled()) {
log.debug("creating pending RSocket for " + exchange.getRoutingMetadata());
}
PendingRequestRSocket pending = constructPendingRSocket(exchange);
Disposable disposable = this.routingTable.addListener(pending);
pending.setSubscriptionDisposable(disposable);
return Mono.just(pending);
}
protected PendingRequestRSocket constructPendingRSocket(GatewayExchange exchange) {
Function<RoutingTable.RegisteredEvent, Mono<Route>> routeFinder = registeredEvent -> getRouteMono(
registeredEvent, exchange);
Consumer<TagsMetadata> tagsMetadataConsumer = tagsMetadata -> {
Tags tags = exchange.getTags().and("responder.id", tagsMetadata.getRouteId());
exchange.setTags(tags);
};
return new PendingRequestRSocket(metadataExtractor, routeFinder,
tagsMetadataConsumer);
}
/**
* Finds routes using exchange of original request that created pending RSocket.
* @param registeredEvent newly registered event
* @param exchange from original request
* @return route if route matches
*/
protected Mono<Route> getRouteMono(RoutingTable.RegisteredEvent registeredEvent,
GatewayExchange exchange) {
return this.routes.findRoute(exchange)
.log(PendingRequestRSocket.class.getName() + ".find route pending",
Level.FINEST)
// TODO: can this be replaced with filter?
.flatMap(
route -> matchRoute(route, registeredEvent.getRoutingMetadata()));
}
/**
* Matches route found using original exchange with routeIds from recently registered
* routes.
* @param route route found using original exchange.
* @param tagsMetadata tags from recent registration.
* @return
*/
private Mono<Route> matchRoute(Route route, TagsMetadata tagsMetadata) {
Set<String> routeIds = this.routingTable.findRouteIds(tagsMetadata);
if (routeIds.contains(route.getId())) {
return Mono.just(route);
}
return Mono.empty();
}
}

View File

@@ -1,129 +0,0 @@
/*
* Copyright 2018-2019 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.gateway.rsocket.registry;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import io.rsocket.RSocket;
import io.rsocket.util.RSocketProxy;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
public class LoadBalancedRSocket {
private static final Log log = LogFactory.getLog(LoadBalancedRSocket.class);
private final List<EnrichedRSocket> delegates = new CopyOnWriteArrayList<>();
private final String serviceName;
private final LoadBalancer loadBalancer;
public LoadBalancedRSocket(String serviceName) {
this(serviceName, new RoundRobinLoadBalancer(serviceName));
}
public LoadBalancedRSocket(String serviceName, LoadBalancer loadBalancer) {
this.serviceName = serviceName;
this.loadBalancer = loadBalancer;
}
public Mono<EnrichedRSocket> choose() {
return this.loadBalancer.apply(this.delegates);
}
public void addRSocket(RSocket rsocket, Metadata metadata) {
this.delegates.add(new EnrichedRSocket(rsocket, metadata));
}
public void remove(Metadata metadata) {
// TODO: move delegates to a map for easy removal
this.delegates.stream()
.filter(enriched -> metadata.matches(enriched.getMetadata())).findFirst()
.ifPresent(this.delegates::remove);
}
public List<EnrichedRSocket> getDelegates() {
return this.delegates;
}
public static class EnrichedRSocket extends RSocketProxy {
private final Metadata metadata;
public EnrichedRSocket(RSocket source, Metadata metadata) {
super(source);
this.metadata = metadata;
}
public Metadata getMetadata() {
return this.metadata;
}
public RSocket getSource() {
return this.source;
}
}
// TODO: Flux<RSocket> as input?
// TODO: reuse commons load balancer?
public interface LoadBalancer
extends Function<List<EnrichedRSocket>, Mono<EnrichedRSocket>> {
}
public static class RoundRobinLoadBalancer implements LoadBalancer {
private final AtomicInteger position;
private final String serviceName;
public RoundRobinLoadBalancer(String serviceName) {
this(serviceName, new Random().nextInt(1000));
}
public RoundRobinLoadBalancer(String serviceName, int seedPosition) {
this.serviceName = serviceName;
this.position = new AtomicInteger(seedPosition);
}
@Override
public Mono<EnrichedRSocket> apply(List<EnrichedRSocket> rSockets) {
if (rSockets.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for: " + this.serviceName);
}
return Mono.empty();
}
// TODO: enforce order?
int pos = Math.abs(this.position.incrementAndGet());
EnrichedRSocket rSocket = rSockets.get(pos % rSockets.size());
return Mono.just(rSocket);
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.registry;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import io.rsocket.RSocket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
public class LoadBalancerFactory {
private static final Log log = LogFactory.getLog(LoadBalancerFactory.class);
private final RoutingTable routingTable;
public LoadBalancerFactory(RoutingTable routingTable) {
this.routingTable = routingTable;
}
// TODO: potentially GatewayExchange or return a new Result Object?
public Mono<Tuple2<String, RSocket>> choose(TagsMetadata tagsMetadata) {
List<Tuple2<String, RSocket>> rSockets = this.routingTable
.findRSockets(tagsMetadata);
// TODO: change loadbalancer impl based on tags
// TODO: cache loadbalancers based on tags
return new RoundRobinLoadBalancer(tagsMetadata).apply(rSockets);
}
// TODO: Flux<RSocket> as input?
// TODO: reuse commons load balancer?
public interface LoadBalancer extends
Function<List<Tuple2<String, RSocket>>, Mono<Tuple2<String, RSocket>>> {
}
public static class RoundRobinLoadBalancer implements LoadBalancer {
private final TagsMetadata tagsMetadata;
private final AtomicInteger position;
public RoundRobinLoadBalancer(TagsMetadata tagsMetadata) {
this(tagsMetadata, new Random().nextInt(1000));
}
public RoundRobinLoadBalancer(TagsMetadata tagsMetadata, int seedPosition) {
this.tagsMetadata = tagsMetadata;
this.position = new AtomicInteger(seedPosition);
}
@Override
public Mono<Tuple2<String, RSocket>> apply(
List<Tuple2<String, RSocket>> rSockets) {
if (rSockets.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for: " + this.tagsMetadata);
}
return Mono.empty();
}
// TODO: enforce order?
int pos = Math.abs(this.position.incrementAndGet());
Tuple2<String, RSocket> tuple = rSockets.get(pos % rSockets.size());
return Mono.just(tuple);
}
}
}

View File

@@ -1,110 +0,0 @@
/*
* Copyright 2018-2019 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.gateway.rsocket.registry;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import io.rsocket.RSocket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.Disposable;
import reactor.core.publisher.DirectProcessor;
import reactor.core.publisher.FluxSink;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.util.Assert;
/**
* The Registry handles all RSocket connections that have been made that have associated
* announcement metadata. RSocket connections can then be found based on routing metadata.
* When a new RSocket is registered, a RegisteredEvent is pushed onto a DirectProcessor
* that is acting as an event bus for registered Consumers.
*/
// TODO: name?
public class Registry {
private static final Log log = LogFactory.getLog(Registry.class);
private final Map<String, LoadBalancedRSocket> rsockets = new ConcurrentHashMap<>();
private final DirectProcessor<RegisteredEvent> registeredEvents = DirectProcessor
.create();
private final FluxSink<RegisteredEvent> registeredEventsSink = registeredEvents
.sink(FluxSink.OverflowStrategy.DROP);
public Registry() {
}
// TODO: Mono<Void>?
public void register(Metadata metadata, RSocket rsocket) {
Assert.notNull(metadata, "metadata may not be null");
Assert.notNull(rsocket, "RSocket may not be null");
if (log.isDebugEnabled()) {
log.debug("Registering RSocket: " + metadata);
}
LoadBalancedRSocket composite = rsockets.computeIfAbsent(metadata.getName(),
s -> new LoadBalancedRSocket(metadata.getName()));
composite.addRSocket(rsocket, metadata);
registeredEventsSink.next(new RegisteredEvent(metadata, rsocket));
}
public void deregister(Metadata metadata) {
Assert.notNull(metadata, "metadata may not be null");
if (log.isDebugEnabled()) {
log.debug("Deregistering RSocket: " + metadata);
}
LoadBalancedRSocket loadBalanced = this.rsockets.get(metadata.getName());
if (loadBalanced != null) {
loadBalanced.remove(metadata);
}
}
public LoadBalancedRSocket getRegistered(Metadata metadata) {
return rsockets.get(metadata.getName());
}
public Disposable addListener(Consumer<RegisteredEvent> consumer) {
return this.registeredEvents.subscribe(consumer);
}
public static class RegisteredEvent {
private final Metadata routingMetadata;
private final RSocket rSocket;
public RegisteredEvent(Metadata routingMetadata, RSocket rSocket) {
Assert.notNull(routingMetadata, "routingMetadata may not be null");
Assert.notNull(rSocket, "RSocket may not be null");
this.routingMetadata = routingMetadata;
this.rSocket = rSocket;
}
public Metadata getRoutingMetadata() {
return routingMetadata;
}
public RSocket getRSocket() {
return rSocket;
}
}
}

View File

@@ -1,90 +0,0 @@
/*
* Copyright 2018-2019 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.gateway.rsocket.registry;
import java.util.Collection;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.rsocket.route.Route;
import org.springframework.cloud.gateway.rsocket.route.Routes;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
/**
* Creates routes from RegisteredEvents.
*/
public class RegistryRoutes implements Routes, Consumer<Registry.RegisteredEvent> {
private static final Log log = LogFactory.getLog(RegistryRoutes.class);
private Map<String, Route> routes = new ConcurrentHashMap<>();
@Override
public Flux<Route> getRoutes() {
// TODO: sorting
// TODO: caching
Collection<Route> routeCollection = routes.values();
if (log.isDebugEnabled()) {
log.debug("Found routes: " + routeCollection);
}
return Flux.fromIterable(routeCollection);
}
@Override
public void accept(Registry.RegisteredEvent registeredEvent) {
Metadata routingMetadata = registeredEvent.getRoutingMetadata();
String id = getId(routingMetadata);
routes.computeIfAbsent(id, key -> createRoute(id, routingMetadata));
}
private String getId(Metadata routingMetadata) {
String id = routingMetadata.getName();
if (id == null) {
id = UUID.randomUUID().toString();
}
return id;
}
private Route createRoute(String id, Metadata routingMetadata) {
Route route = Route.builder().id(id).routingMetadata(routingMetadata)
.predicate(exchange -> {
// TODO: standard predicates
// TODO: allow customized predicates
Metadata incomingRouting = exchange.getRoutingMetadata();
boolean matches = incomingRouting.getName()
.equalsIgnoreCase(routingMetadata.getName());
return Mono.just(matches);
})
// TODO: allow customized filters
.build();
if (log.isDebugEnabled()) {
log.debug("Created Route for registered service " + route);
}
return route;
}
}

View File

@@ -22,25 +22,26 @@ import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorEx
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilter;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilterChain;
import org.springframework.core.Ordered;
import org.springframework.util.StringUtils;
/**
* Filter that registers the SendingSocket.
*/
public class RegistrySocketAcceptorFilter implements SocketAcceptorFilter, Ordered {
private final Registry registry;
private final RoutingTable routingTable;
public RegistrySocketAcceptorFilter(Registry registry) {
this.registry = registry;
public RegistrySocketAcceptorFilter(RoutingTable routingTable) {
this.routingTable = routingTable;
}
@Override
public Mono<Success> filter(SocketAcceptorExchange exchange,
SocketAcceptorFilterChain chain) {
if (exchange.getMetadata() != null
&& StringUtils.hasLength(exchange.getMetadata().getName())) {
this.registry.register(exchange.getMetadata(), exchange.getSendingSocket());
if (exchange.getMetadata() != null) {
// TODO: needed? &&
// StringUtils.hasLength(exchange.getMetadata().getServiceName())) {
this.routingTable.register(exchange.getMetadata().getEnrichedTagsMetadata(),
exchange.getSendingSocket());
}
return chain.filter(exchange);

View File

@@ -0,0 +1,287 @@
/*
* Copyright 2018-2019 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.gateway.rsocket.registry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import io.rsocket.RSocket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.roaringbitmap.IntConsumer;
import org.roaringbitmap.RoaringBitmap;
import reactor.core.Disposable;
import reactor.core.publisher.DirectProcessor;
import reactor.core.publisher.FluxSink;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.cloud.gateway.rsocket.support.WellKnownKey;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* The RoutingTable handles all RSocket connections that have been made that have
* associated announcement metadata. RSocket connections can then be found based on
* routing metadata. When a new RSocket is registered, a RegisteredEvent is pushed onto a
* DirectProcessor that is acting as an event bus for registered Consumers.
*/
public class RoutingTable {
private static final Log log = LogFactory.getLog(RoutingTable.class);
AtomicInteger internalRouteId = new AtomicInteger();
final Map<Integer, String> internalRouteIdToRouteId = new ConcurrentHashMap<>();
final Map<RegistryKey, RoaringBitmap> tagsToBitmaps = new ConcurrentHashMap<>();
final Map<String, RSocket> routeIdToRSocket = new ConcurrentHashMap<>();
final Map<String, TagsMetadata> routeIdToTags = new ConcurrentHashMap<>();
private final DirectProcessor<RegisteredEvent> registeredEvents = DirectProcessor
.create();
private final FluxSink<RegisteredEvent> registeredEventsSink = registeredEvents
.sink(FluxSink.OverflowStrategy.DROP);
public RoutingTable() {
}
// TODO: Mono<Void>?
public void register(TagsMetadata tagsMetadata, RSocket rsocket) {
Assert.notNull(tagsMetadata, "tagsMetadata may not be null");
Assert.notNull(rsocket, "RSocket may not be null");
if (log.isDebugEnabled()) {
log.debug("Registering RSocket: " + tagsMetadata);
}
// TODO: only register new route if timestamp is newer
String routeId = tagsMetadata.getRouteId();
if (routeIdToRSocket.containsKey(routeId)) {
throw new IllegalStateException("Route Id already registered: " + routeId);
}
int internalId = internalRouteId.incrementAndGet();
internalRouteIdToRouteId.put(internalId, routeId);
routeIdToRSocket.put(routeId, rsocket);
routeIdToTags.put(routeId, tagsMetadata);
tagsMetadata.getTags().forEach((key, value) -> {
// TODO: deal with string keys?
RoaringBitmap bitmap = tagsToBitmaps.computeIfAbsent(
new RegistryKey(key, value), k -> new RoaringBitmap());
bitmap.add(internalId);
});
registeredEventsSink.next(new RegisteredEvent(tagsMetadata, rsocket));
}
public boolean deregister(TagsMetadata metadata) {
Assert.notNull(metadata, "metadata may not be null");
String routeId = metadata.getRouteId();
if (!StringUtils.hasText(routeId)) {
if (log.isDebugEnabled()) {
log.debug("Unable to deregister, no RouteId: " + metadata);
}
return false;
}
if (log.isDebugEnabled()) {
log.debug("Deregistering RSocket: " + metadata);
}
TagsMetadata findByRouteId = TagsMetadata.builder()
.with(WellKnownKey.ROUTE_ID, routeId).build();
RoaringBitmap found = find(findByRouteId);
if (found.isEmpty() || found.getLongCardinality() > 1) {
if (log.isWarnEnabled()) {
log.warn("Unable to deregister " + metadata + ", found: "
+ found.getLongCardinality());
}
return false;
}
int internalId = found.first();
internalRouteIdToRouteId.remove(internalId);
routeIdToTags.remove(routeId);
routeIdToRSocket.remove(routeId);
metadata.getTags().forEach((key, value) -> {
// TODO: deal with string keys?
RegistryKey registryKey = new RegistryKey(key, value);
if (tagsToBitmaps.containsKey(registryKey)) {
RoaringBitmap bitmap = tagsToBitmaps.get(registryKey);
bitmap.remove(internalId);
}
});
// TODO: deregistered event
return true;
}
/**
* Finds routeIds of matching routes.
* @param tagsMetadata tags to match.
* @return all matching routeIds or empty list.
*/
public Set<String> findRouteIds(TagsMetadata tagsMetadata) {
RoaringBitmap found = find(tagsMetadata);
if (found.isEmpty()) {
return Collections.emptySet();
}
HashSet<String> routeIds = new HashSet<>();
found.forEach((IntConsumer) internalId -> {
String routeId = internalRouteIdToRouteId.get(internalId);
routeIds.add(routeId);
});
return routeIds;
}
/**
* Finds tuples of routeIds and RSockets of matching routes.
* @param tagsMetadata tags to match.
* @return all matching routeId and RSocket tuples or empty list.
*/
public List<Tuple2<String, RSocket>> findRSockets(TagsMetadata tagsMetadata) {
RoaringBitmap found = find(tagsMetadata);
if (found.isEmpty()) {
return Collections.emptyList();
}
ArrayList<Tuple2<String, RSocket>> rSockets = new ArrayList<>();
found.forEach((IntConsumer) internalId -> {
String routeId = internalRouteIdToRouteId.get(internalId);
RSocket rSocket = routeIdToRSocket.get(routeId);
rSockets.add(Tuples.of(routeId, rSocket));
});
return rSockets;
}
/**
* Finds internal ids of routes.
* @param tagsMetadata tags to match
* @return bitmap of all internal ids of routes.
*/
RoaringBitmap find(TagsMetadata tagsMetadata) {
RoaringBitmap found = new RoaringBitmap();
AtomicBoolean first = new AtomicBoolean(true);
tagsMetadata.getTags().forEach((key, value) -> {
RegistryKey registryKey = new RegistryKey(key, value);
if (tagsToBitmaps.containsKey(registryKey)) {
RoaringBitmap search = tagsToBitmaps.get(registryKey);
if (first.get()) {
// initiliaze found bitmap with current search
found.or(search);
first.compareAndSet(true, false);
}
else {
found.and(search);
}
}
});
return found;
}
public Disposable addListener(Consumer<RegisteredEvent> consumer) {
return this.registeredEvents.subscribe(consumer);
}
public static class RegisteredEvent {
private final TagsMetadata routingMetadata;
private final RSocket rSocket;
public RegisteredEvent(TagsMetadata routingMetadata, RSocket rSocket) {
Assert.notNull(routingMetadata, "routingMetadata may not be null");
Assert.notNull(rSocket, "RSocket may not be null");
this.routingMetadata = routingMetadata;
this.rSocket = rSocket;
}
public TagsMetadata getRoutingMetadata() {
return routingMetadata;
}
public RSocket getRSocket() {
return rSocket;
}
}
static class RegistryKey {
final TagsMetadata.Key key;
final String value;
RegistryKey(TagsMetadata.Key key, String value) {
// TODO: Assert non null
this.key = key;
this.value = value.toLowerCase();
}
public TagsMetadata.Key getKey() {
return this.key;
}
public String getValue() {
return this.value;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RegistryKey that = (RegistryKey) o;
return Objects.equals(this.key, that.key)
&& Objects.equals(this.value, that.value);
}
@Override
public int hashCode() {
return Objects.hash(this.key, this.value);
}
@Override
public String toString() {
return new ToStringCreator(this).append("key", key).append("value", value)
.toString();
}
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright 2018-2019 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.gateway.rsocket.registry;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.rsocket.core.GatewayExchange;
import org.springframework.cloud.gateway.rsocket.core.GatewayFilter;
import org.springframework.cloud.gateway.rsocket.route.Route;
import org.springframework.cloud.gateway.rsocket.route.Routes;
import org.springframework.cloud.gateway.rsocket.support.AsyncPredicate;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.core.style.ToStringCreator;
/**
* View of RoutingTable as Route objects.
*/
public class RoutingTableRoutes
implements Routes, Consumer<RoutingTable.RegisteredEvent> {
private static final Log log = LogFactory.getLog(RoutingTableRoutes.class);
private Map<String, Route> routes = new ConcurrentHashMap<>();
private final RoutingTable routingTable;
public RoutingTableRoutes(RoutingTable routingTable) {
this.routingTable = routingTable;
this.routingTable.addListener(this);
}
@Override
public Flux<Route> getRoutes() {
// TODO: sorting?
// TODO: caching
Collection<Route> routeCollection = routes.values();
if (log.isDebugEnabled()) {
log.debug("Found routes: " + routeCollection);
}
return Flux.fromIterable(routeCollection);
}
@Override
public void accept(RoutingTable.RegisteredEvent registeredEvent) {
TagsMetadata routingMetadata = registeredEvent.getRoutingMetadata();
String routeId = routingMetadata.getRouteId();
routes.computeIfAbsent(routeId, key -> createRoute(routeId));
}
private Route createRoute(String id) {
AsyncPredicate<GatewayExchange> predicate = exchange -> {
// TODO: standard predicates
// TODO: allow customized predicates
Set<String> routeIds = routingTable
.findRouteIds(exchange.getRoutingMetadata());
return Mono.just(routeIds.contains(id));
};
RegistryRoute route = new RegistryRoute(id, predicate);
if (log.isDebugEnabled()) {
log.debug("Created Route for registered service " + route);
}
return route;
}
static class RegistryRoute implements Route {
final String id;
final AsyncPredicate<GatewayExchange> predicate;
RegistryRoute(String id, AsyncPredicate<GatewayExchange> predicate) {
this.id = id;
this.predicate = predicate;
}
@Override
public String getId() {
return this.id;
}
@Override
public AsyncPredicate<GatewayExchange> getPredicate() {
return this.predicate;
}
@Override
public List<GatewayFilter> getFilters() {
return Collections.emptyList();
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
RegistryRoute that = (RegistryRoute) o;
return Objects.equals(this.id, that.id)
&& Objects.equals(this.predicate, that.predicate);
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.predicate);
}
@Override
public String toString() {
return new ToStringCreator(this).append("id", id)
.append("predicate", predicate).toString();
}
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2018-2019 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.gateway.rsocket.route;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.springframework.cloud.gateway.rsocket.core.GatewayExchange;
import org.springframework.cloud.gateway.rsocket.core.GatewayFilter;
import org.springframework.cloud.gateway.rsocket.support.AsyncPredicate;
import org.springframework.cloud.gateway.rsocket.support.RouteSetup;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
/**
* @author Spencer Gibb
*/
public class DefaultRoute implements Route {
private final String id;
private final RouteSetup targetMetadata;
private final int order;
private final AsyncPredicate<GatewayExchange> predicate;
private final List<GatewayFilter> gatewayFilters;
public static Builder builder() {
return new Builder();
}
private DefaultRoute(String id, RouteSetup targetMetadata, int order,
AsyncPredicate<GatewayExchange> predicate,
List<GatewayFilter> gatewayFilters) {
this.id = id;
this.targetMetadata = targetMetadata;
this.order = order;
this.predicate = predicate;
this.gatewayFilters = gatewayFilters;
}
public String getId() {
return this.id;
}
public int getOrder() {
return order;
}
public AsyncPredicate<GatewayExchange> getPredicate() {
return this.predicate;
}
public List<GatewayFilter> getFilters() {
return Collections.unmodifiableList(this.gatewayFilters);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Route route = (Route) o;
return Objects.equals(id, route.getId())
&& Objects.equals(order, route.getOrder())
&& Objects.equals(predicate, route.getPredicate())
&& Objects.equals(gatewayFilters, route.getFilters());
}
@Override
public int hashCode() {
return Objects.hash(id, targetMetadata, predicate, gatewayFilters);
}
@Override
public String toString() {
return new ToStringCreator(this).append("id", id)
.append("targetMetadata", targetMetadata).append("order", order)
.append("predicate", predicate).append("gatewayFilters", gatewayFilters)
.toString();
}
public static class Builder {
protected String id;
protected RouteSetup routingMetadata;
protected int order = 0;
protected AsyncPredicate<GatewayExchange> predicate;
protected List<GatewayFilter> gatewayFilters = new ArrayList<>();
protected Builder() {
}
public Builder id(String id) {
this.id = id;
return this;
}
public String getId() {
return id;
}
public Builder order(int order) {
this.order = order;
return this;
}
public AsyncPredicate<GatewayExchange> getPredicate() {
return this.predicate;
}
public Builder routingMetadata(RouteSetup routingMetadata) {
this.routingMetadata = routingMetadata;
return this;
}
public Builder setFilters(List<GatewayFilter> gatewayFilters) {
this.gatewayFilters = gatewayFilters;
return this;
}
public Builder filter(GatewayFilter gatewayFilter) {
this.gatewayFilters.add(gatewayFilter);
return this;
}
public Builder filters(Collection<GatewayFilter> gatewayFilters) {
this.gatewayFilters.addAll(gatewayFilters);
return this;
}
public Builder filters(GatewayFilter... gatewayFilters) {
return filters(Arrays.asList(gatewayFilters));
}
public Builder predicate(AsyncPredicate<GatewayExchange> predicate) {
this.predicate = predicate;
return this;
}
public Builder and(AsyncPredicate<GatewayExchange> predicate) {
Assert.notNull(this.predicate, "can not call and() on null predicate");
this.predicate = this.predicate.and(predicate);
return this;
}
public Builder or(AsyncPredicate<GatewayExchange> predicate) {
Assert.notNull(this.predicate, "can not call or() on null predicate");
this.predicate = this.predicate.or(predicate);
return this;
}
public Builder negate() {
Assert.notNull(this.predicate, "can not call negate() on null predicate");
this.predicate = this.predicate.negate();
return this;
}
public Route build() {
Assert.notNull(this.id, "id can not be null");
Assert.notNull(this.routingMetadata, "targetMetadata can not be null");
Assert.notNull(this.predicate, "predicate can not be null");
return new DefaultRoute(this.id, this.routingMetadata, this.order, predicate,
this.gatewayFilters);
}
}
}

View File

@@ -16,188 +16,26 @@
package org.springframework.cloud.gateway.rsocket.route;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.springframework.cloud.gateway.rsocket.core.GatewayExchange;
import org.springframework.cloud.gateway.rsocket.core.GatewayFilter;
import org.springframework.cloud.gateway.rsocket.support.AsyncPredicate;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.core.Ordered;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
/**
* @author Spencer Gibb
*/
public class Route implements Ordered {
public interface Route extends Ordered {
private final String id;
String getId();
private final Metadata targetMetadata;
private final int order;
private final AsyncPredicate<GatewayExchange> predicate;
private final List<GatewayFilter> gatewayFilters;
public static Builder builder() {
return new Builder();
default int getOrder() {
return 0;
}
private Route(String id, Metadata targetMetadata, int order,
AsyncPredicate<GatewayExchange> predicate,
List<GatewayFilter> gatewayFilters) {
this.id = id;
this.targetMetadata = targetMetadata;
this.order = order;
this.predicate = predicate;
this.gatewayFilters = gatewayFilters;
}
AsyncPredicate<GatewayExchange> getPredicate();
public String getId() {
return this.id;
}
public Metadata getTargetMetadata() {
return this.targetMetadata;
}
public int getOrder() {
return order;
}
public AsyncPredicate<GatewayExchange> getPredicate() {
return this.predicate;
}
public List<GatewayFilter> getFilters() {
return Collections.unmodifiableList(this.gatewayFilters);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Route route = (Route) o;
return Objects.equals(id, route.id)
&& Objects.equals(targetMetadata, route.targetMetadata)
&& Objects.equals(order, route.order)
&& Objects.equals(predicate, route.predicate)
&& Objects.equals(gatewayFilters, route.gatewayFilters);
}
@Override
public int hashCode() {
return Objects.hash(id, targetMetadata, predicate, gatewayFilters);
}
@Override
public String toString() {
return new ToStringCreator(this).append("id", id)
.append("targetMetadata", targetMetadata).append("order", order)
.append("predicate", predicate).append("gatewayFilters", gatewayFilters)
.toString();
}
public static class Builder {
protected String id;
protected Metadata routingMetadata;
protected int order = 0;
protected AsyncPredicate<GatewayExchange> predicate;
protected List<GatewayFilter> gatewayFilters = new ArrayList<>();
protected Builder() {
}
public Builder id(String id) {
this.id = id;
return this;
}
public String getId() {
return id;
}
public Builder order(int order) {
this.order = order;
return this;
}
public AsyncPredicate<GatewayExchange> getPredicate() {
return this.predicate;
}
public Builder routingMetadata(Metadata routingMetadata) {
this.routingMetadata = routingMetadata;
return this;
}
public Builder setFilters(List<GatewayFilter> gatewayFilters) {
this.gatewayFilters = gatewayFilters;
return this;
}
public Builder filter(GatewayFilter gatewayFilter) {
this.gatewayFilters.add(gatewayFilter);
return this;
}
public Builder filters(Collection<GatewayFilter> gatewayFilters) {
this.gatewayFilters.addAll(gatewayFilters);
return this;
}
public Builder filters(GatewayFilter... gatewayFilters) {
return filters(Arrays.asList(gatewayFilters));
}
public Builder predicate(AsyncPredicate<GatewayExchange> predicate) {
this.predicate = predicate;
return this;
}
public Builder and(AsyncPredicate<GatewayExchange> predicate) {
Assert.notNull(this.predicate, "can not call and() on null predicate");
this.predicate = this.predicate.and(predicate);
return this;
}
public Builder or(AsyncPredicate<GatewayExchange> predicate) {
Assert.notNull(this.predicate, "can not call or() on null predicate");
this.predicate = this.predicate.or(predicate);
return this;
}
public Builder negate() {
Assert.notNull(this.predicate, "can not call negate() on null predicate");
this.predicate = this.predicate.negate();
return this;
}
public Route build() {
Assert.notNull(this.id, "id can not be null");
Assert.notNull(this.routingMetadata, "targetMetadata can not be null");
Assert.notNull(this.predicate, "predicate can not be null");
return new Route(this.id, this.routingMetadata, this.order, predicate,
this.gatewayFilters);
}
}
List<GatewayFilter> getFilters();
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.gateway.rsocket.socketacceptor;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -31,9 +32,12 @@ import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.rsocket.autoconfigure.GatewayRSocketProperties;
import org.springframework.cloud.gateway.rsocket.core.GatewayRSocket;
import org.springframework.cloud.gateway.rsocket.core.GatewayRSocketFactory;
import org.springframework.cloud.gateway.rsocket.metrics.MicrometerResponderRSocket;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.cloud.gateway.rsocket.support.RouteSetup;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.util.MimeType;
public class GatewaySocketAcceptor implements SocketAcceptor {
@@ -41,19 +45,22 @@ public class GatewaySocketAcceptor implements SocketAcceptor {
private final SocketAcceptorFilterChain filterChain;
private final GatewayRSocket.Factory rSocketFactory;
private final GatewayRSocketFactory rSocketFactory;
private final MeterRegistry meterRegistry;
private final GatewayRSocketProperties properties;
public GatewaySocketAcceptor(GatewayRSocket.Factory rSocketFactory,
private final MetadataExtractor metadataExtractor;
public GatewaySocketAcceptor(GatewayRSocketFactory rSocketFactory,
List<SocketAcceptorFilter> filters, MeterRegistry meterRegistry,
GatewayRSocketProperties properties) {
GatewayRSocketProperties properties, MetadataExtractor metadataExtractor) {
this.rSocketFactory = rSocketFactory;
this.filterChain = new SocketAcceptorFilterChain(filters);
this.meterRegistry = meterRegistry;
this.properties = properties;
this.metadataExtractor = metadataExtractor;
}
@Override
@@ -70,11 +77,22 @@ public class GatewaySocketAcceptor implements SocketAcceptor {
Tags metadataTags;
SocketAcceptorExchange exchange;
if (setup.hasMetadata()) { // TODO: and setup.metadataMimeType() is Announcement
// metadata or composite
Metadata metadata = Metadata.decodeMetadata(setup.sliceMetadata());
metadataTags = Tags.of("service.name", metadata.getName()).and("service.id",
metadata.get("id"));
Map<String, Object> metadataMap = null;
try {
metadataMap = this.metadataExtractor.extract(setup,
MimeType.valueOf(setup.metadataMimeType()));
}
catch (Exception e) {
if (log.isDebugEnabled()) {
log.debug("Error extracting metadata", e);
}
return Mono.error(e);
}
if (metadataMap.containsKey("routesetup")) {
RouteSetup metadata = (RouteSetup) metadataMap.get("routesetup");
metadataTags = Tags.of("service.name", metadata.getServiceName())
.and("service.id", metadata.getId().toString());
// enrich exchange to have metadata
exchange = new SocketAcceptorExchange(setup,
decorate(sendingSocket, requesterTags.and(metadataTags)), metadata);
@@ -92,12 +110,12 @@ public class GatewaySocketAcceptor implements SocketAcceptor {
// decorate with metrics gateway id, type responder, service name, service id
// (instance id)
return this.filterChain.filter(exchange)
.log(GatewaySocketAcceptor.class.getName()
+ ".socket acceptor filter chain", Level.FINEST)
.map(success -> decorate(
this.rSocketFactory.create(exchange.getMetadata()),
responderTags));
return this.filterChain.filter(exchange).log(
GatewaySocketAcceptor.class.getName() + ".socket acceptor filter chain",
Level.FINEST).map(success -> {
TagsMetadata tags = exchange.getMetadata().getEnrichedTagsMetadata();
return decorate(this.rSocketFactory.create(tags), responderTags);
});
}
private RSocket decorate(RSocket rSocket, Tags tags) {

View File

@@ -22,7 +22,7 @@ import io.rsocket.ConnectionSetupPayload;
import io.rsocket.RSocket;
import org.springframework.cloud.gateway.rsocket.filter.AbstractRSocketExchange;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.cloud.gateway.rsocket.support.RouteSetup;
public class SocketAcceptorExchange extends AbstractRSocketExchange {
@@ -30,14 +30,14 @@ public class SocketAcceptorExchange extends AbstractRSocketExchange {
private final RSocket sendingSocket;
private final Metadata metadata;
private final RouteSetup metadata;
public SocketAcceptorExchange(ConnectionSetupPayload setup, RSocket sendingSocket) {
this(setup, sendingSocket, new Metadata(null, Collections.emptyMap()));
this(setup, sendingSocket, new RouteSetup(null, null, Collections.emptyMap()));
}
public SocketAcceptorExchange(ConnectionSetupPayload setup, RSocket sendingSocket,
Metadata metadata) {
RouteSetup metadata) {
this.setup = setup;
this.sendingSocket = sendingSocket;
this.metadata = metadata;
@@ -51,7 +51,7 @@ public class SocketAcceptorExchange extends AbstractRSocketExchange {
return sendingSocket;
}
public Metadata getMetadata() {
public RouteSetup getMetadata() {
return metadata;
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import java.math.BigInteger;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
// TODO: currently an ENVELOPE frame in RSocket extension, also discarding metadata
public class Forwarding extends TagsMetadata {
/**
* Forwarding subtype.
*/
public static final String FORWARDING = "x.rsocket.forwarding.v0";
/**
* Forwarding mimetype.
*/
public static final MimeType FORWARDING_MIME_TYPE = new MimeType("message",
FORWARDING);
private final BigInteger originRouteId;
public Forwarding(long originRouteId, Map<TagsMetadata.Key, String> tags) {
this(BigInteger.valueOf(originRouteId), tags);
}
public Forwarding(BigInteger originRouteId, Map<TagsMetadata.Key, String> tags) {
super(tags);
this.originRouteId = originRouteId;
}
public BigInteger getOriginRouteId() {
return this.originRouteId;
}
public ByteBuf encode() {
return encode(this);
}
@Override
public String toString() {
// @formatter:off
return new ToStringCreator(this)
.append("originRouteId", originRouteId)
.append("tags", getTags())
.toString();
// @formatter:on
}
static ByteBuf encode(Forwarding forwarding) {
return encode(ByteBufAllocator.DEFAULT, forwarding);
}
static ByteBuf encode(ByteBufAllocator allocator, Forwarding forwarding) {
Assert.notNull(forwarding, "forwarding may not be null");
Assert.notNull(allocator, "allocator may not be null");
ByteBuf byteBuf = allocator.buffer();
encodeBigInteger(byteBuf, forwarding.originRouteId);
encode(byteBuf, forwarding.getTags());
return byteBuf;
}
static Forwarding decode(ByteBuf byteBuf) {
AtomicInteger offset = new AtomicInteger(0);
BigInteger originRouteId = decodeBigInteger(byteBuf, offset);
TagsMetadata tagsMetadata = decode(offset, byteBuf);
Forwarding forwarding = new Forwarding(originRouteId, tagsMetadata.getTags());
return forwarding;
}
public static class Encoder extends AbstractEncoder<Forwarding> {
public Encoder() {
super(Forwarding.FORWARDING_MIME_TYPE);
}
@Override
public Flux<DataBuffer> encode(Publisher<? extends Forwarding> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType,
MimeType mimeType, Map<String, Object> hints) {
throw new UnsupportedOperationException("stream encoding not supported.");
}
@Override
public DataBuffer encodeValue(Forwarding value, DataBufferFactory bufferFactory,
ResolvableType valueType, MimeType mimeType, Map<String, Object> hints) {
NettyDataBufferFactory factory = (NettyDataBufferFactory) bufferFactory;
ByteBuf encoded = Forwarding.encode(factory.getByteBufAllocator(), value);
return factory.wrap(encoded);
}
}
public static class Decoder extends AbstractDecoder<Forwarding> {
public Decoder() {
super(Forwarding.FORWARDING_MIME_TYPE);
}
@Override
public Flux<Forwarding> decode(Publisher<DataBuffer> inputStream,
ResolvableType elementType, MimeType mimeType,
Map<String, Object> hints) {
throw new UnsupportedOperationException("stream decoding not supported.");
}
@Override
public Forwarding decode(DataBuffer buffer, ResolvableType targetType,
MimeType mimeType, Map<String, Object> hints) throws DecodingException {
ByteBuf byteBuf = TagsMetadata.asByteBuf(buffer);
return Forwarding.decode(byteBuf);
}
}
}

View File

@@ -16,192 +16,16 @@
package org.springframework.cloud.gateway.rsocket.support;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import io.rsocket.metadata.WellKnownMimeType;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.rsocket.util.NumberUtils;
import org.springframework.util.MimeType;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
public class Metadata {
public abstract class Metadata {
/**
* Mime type of routing extension.
* Composite Metadata MimeType.
*/
public static final String ROUTING_MIME_TYPE = "message/x.rsocket.routing.v0";
/**
* The logical name.
*/
private final String name;
/**
* Keys and values associated with name.
*/
private final Map<String, String> properties;
public Metadata(String name, Map<String, String> properties) {
this.name = name;
this.properties = properties;
}
public String getName() {
return this.name;
}
public Map<String, String> getProperties() {
return this.properties;
}
public String get(String key) {
return this.properties.get(key);
}
public String put(String key, String value) {
return this.properties.put(key, value);
}
@Override
public String toString() {
return new ToStringCreator(this).append("name", name)
.append("properties", properties).toString();
}
public static Builder from(String name) {
return new Builder(name);
}
public static ByteBuf encode(Metadata metadata) {
return encode(ByteBufAllocator.DEFAULT, metadata);
}
public static ByteBuf encode(ByteBufAllocator allocator, Metadata metadata) {
return encode(allocator, metadata.getName(), metadata.getProperties());
}
public static ByteBuf encode(String name, Map<String, String> properties) {
return encode(ByteBufAllocator.DEFAULT, name, properties);
}
public static ByteBuf encode(ByteBufAllocator allocator, String name,
Map<String, String> properties) {
Assert.hasText(name, "name may not be empty");
Assert.notNull(properties, "properties may not be null");
Assert.notNull(allocator, "allocator may not be null");
ByteBuf byteBuf = allocator.buffer();
encodeString(byteBuf, name);
properties.entrySet().stream().forEach(entry -> {
encodeString(byteBuf, entry.getKey());
encodeString(byteBuf, entry.getValue());
});
return byteBuf;
}
private static void encodeString(ByteBuf byteBuf, String s) {
int length = NumberUtils.requireUnsignedByte(ByteBufUtil.utf8Bytes(s));
byteBuf.writeByte(length);
ByteBufUtil.reserveAndWriteUtf8(byteBuf, s, length);
}
public static Metadata decodeMetadata(ByteBuf byteBuf) {
AtomicInteger offset = new AtomicInteger(0);
String name = decodeString(byteBuf, offset);
Map<String, String> properties = new LinkedHashMap<>();
while (offset.get() < byteBuf.readableBytes()) { // TODO: What is the best
// conditional here?
String key = decodeString(byteBuf, offset);
String value = null;
if (offset.get() < byteBuf.readableBytes()) {
value = decodeString(byteBuf, offset);
}
properties.put(key, value);
}
return new Metadata(name, properties);
}
private static String decodeString(ByteBuf byteBuf, AtomicInteger offset) {
int length = byteBuf.getByte(offset.get());
int index = offset.addAndGet(Byte.BYTES);
String s = byteBuf.toString(index, length, StandardCharsets.UTF_8);
offset.addAndGet(length);
return s;
}
public boolean matches(Metadata other) {
if (other == null) {
return false;
}
if (other.getName() == null) {
return false;
}
if (!getName().equalsIgnoreCase(other.getName())) {
return false;
}
return matches(getProperties(), other.getProperties());
}
/**
* Matches leftMetadata to rightMetadata. rightMetadata must contain all key with
* equal values (ignoring case) of leftMetadata.
* @param leftMetadata first metadata to compare.
* @param rightMetadata second metadata to compare.
* @return true if all keys and values (case-insensitive) from leftMetadata are in
* rightMetadata.
*/
// TODO: find a way to make this more performant
public static boolean matches(Map<String, String> leftMetadata,
Map<String, String> rightMetadata) {
if (leftMetadata == null || rightMetadata == null) {
return false;
}
for (Map.Entry<String, String> entry : leftMetadata.entrySet()) {
String enrichedValue = rightMetadata.get(entry.getKey());
if (enrichedValue == null ||
// TODO: regex and possibly SpEL?
!enrichedValue.equalsIgnoreCase(entry.getValue())) {
return false;
}
}
// all entries in metadata exist and match corresponding entries in
// enriched.metadata
return true;
}
public static class Builder {
private final Metadata metadata;
public Builder(String name) {
Assert.hasText(name, "Name must not be empty.");
this.metadata = new Metadata(name, new LinkedHashMap<>());
}
public Builder with(String key, String value) {
this.metadata.put(key, value);
return this;
}
public Metadata build() {
return this.metadata;
}
public ByteBuf encode() {
return Metadata.encode(build());
}
}
public static final MimeType COMPOSITE_MIME_TYPE = MimeType
.valueOf(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.toString());
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import java.math.BigInteger;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.AbstractDecoder;
import org.springframework.core.codec.AbstractEncoder;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.MimeType;
public class RouteSetup extends TagsMetadata {
/**
* Route Setup subtype.
*/
public static final String ROUTE_SETUP = "x.rsocket.routesetup.v0";
/**
* Route Setup mime type.
*/
public static final MimeType ROUTE_SETUP_MIME_TYPE = new MimeType("message",
ROUTE_SETUP);
private final BigInteger id;
private final String serviceName;
public RouteSetup(long id, String serviceName, Map<Key, String> tags) {
this(BigInteger.valueOf(id), serviceName, tags);
}
public RouteSetup(BigInteger id, String serviceName, Map<Key, String> tags) {
super(tags);
this.id = id;
this.serviceName = serviceName;
}
public BigInteger getId() {
return this.id;
}
public String getServiceName() {
return this.serviceName;
}
public ByteBuf encode() {
return encode(this);
}
@Override
public TagsMetadata getEnrichedTagsMetadata() {
// @formatter:off
TagsMetadata tagsMetadata = TagsMetadata.builder(this)
.with(WellKnownKey.SERVICE_NAME, getServiceName())
.with(WellKnownKey.ROUTE_ID, getId().toString())
.build();
// @formatter:on
return tagsMetadata;
}
@Override
public String toString() {
// @formatter:off
return new ToStringCreator(this)
.append("id", id)
.append("serviceName", serviceName)
.append("tags", getTags())
.toString();
// @formatter:on
}
static ByteBuf encode(RouteSetup routeSetup) {
return encode(ByteBufAllocator.DEFAULT, routeSetup);
}
static ByteBuf encode(ByteBufAllocator allocator, RouteSetup routeSetup) {
Assert.notNull(routeSetup, "routeSetup may not be null");
Assert.notNull(allocator, "allocator may not be null");
ByteBuf byteBuf = allocator.buffer();
encodeBigInteger(byteBuf, routeSetup.id);
encodeString(byteBuf, routeSetup.getServiceName());
encode(byteBuf, routeSetup.getTags());
return byteBuf;
}
static RouteSetup decode(ByteBuf byteBuf) {
AtomicInteger offset = new AtomicInteger(0);
BigInteger id = decodeBigInteger(byteBuf, offset);
String serviceName = decodeString(byteBuf, offset);
TagsMetadata tagsMetadata = decode(offset, byteBuf);
RouteSetup routeSetup = new RouteSetup(id, serviceName, tagsMetadata.getTags());
return routeSetup;
}
public static class Encoder extends AbstractEncoder<RouteSetup> {
public Encoder() {
super(ROUTE_SETUP_MIME_TYPE);
}
@Override
public Flux<DataBuffer> encode(Publisher<? extends RouteSetup> inputStream,
DataBufferFactory bufferFactory, ResolvableType elementType,
MimeType mimeType, Map<String, Object> hints) {
throw new UnsupportedOperationException("stream encoding not supported.");
}
@Override
public DataBuffer encodeValue(RouteSetup value, DataBufferFactory bufferFactory,
ResolvableType valueType, MimeType mimeType, Map<String, Object> hints) {
NettyDataBufferFactory factory = (NettyDataBufferFactory) bufferFactory;
ByteBuf encoded = RouteSetup.encode(factory.getByteBufAllocator(), value);
return factory.wrap(encoded);
}
}
public static class Decoder extends AbstractDecoder<RouteSetup> {
public Decoder() {
super(ROUTE_SETUP_MIME_TYPE);
}
@Override
public Flux<RouteSetup> decode(Publisher<DataBuffer> inputStream,
ResolvableType elementType, MimeType mimeType,
Map<String, Object> hints) {
throw new UnsupportedOperationException("stream decoding not supported.");
}
@Override
public RouteSetup decode(DataBuffer buffer, ResolvableType targetType,
MimeType mimeType, Map<String, Object> hints) throws DecodingException {
ByteBuf byteBuf = TagsMetadata.asByteBuf(buffer);
return RouteSetup.decode(byteBuf);
}
}
}

View File

@@ -0,0 +1,345 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicInteger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.rsocket.util.NumberUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
public class TagsMetadata {
private static final Key ROUTE_ID_KEY = new Key(WellKnownKey.ROUTE_ID);
private static final int WELL_KNOWN_TAG = 0x80;
private static final int HAS_MORE_TAGS = 0x80;
private static final int MAX_TAG_LENGTH = 0x7F;
private final Map<Key, String> tags;
TagsMetadata(Map<Key, String> tags) {
this.tags = tags;
}
public static ByteBuf asByteBuf(DataBuffer buffer) {
return buffer instanceof NettyDataBuffer
? ((NettyDataBuffer) buffer).getNativeBuffer()
: Unpooled.wrappedBuffer(buffer.asByteBuffer());
}
public Map<Key, String> getTags() {
return this.tags;
}
public String getRouteId() {
return this.tags.get(ROUTE_ID_KEY);
}
public String get(WellKnownKey key) {
return this.tags.get(new Key(key));
}
public String put(Key key, String value) {
return this.tags.put(key, value);
}
/**
* Allows subclasses to enrich tags before use.
* @return by default, this.
*/
public TagsMetadata getEnrichedTagsMetadata() {
return this;
}
public String toStrin() {
return new ToStringCreator(this).append("tags", tags).toString();
}
@Override
public String toString() {
return "TagsMetadata" + tags;
}
public static Builder builder() {
return new Builder();
}
public static Builder builder(TagsMetadata existing) {
Builder builder = new Builder();
return builder.with(existing);
}
static ByteBuf encode(TagsMetadata metadata) {
return encode(ByteBufAllocator.DEFAULT, metadata.tags);
}
static ByteBuf encode(ByteBufAllocator allocator, Map<Key, String> tags) {
Assert.notNull(tags, "tags may not be null");
Assert.notNull(allocator, "allocator may not be null");
ByteBuf byteBuf = allocator.buffer();
return encode(byteBuf, tags);
}
static ByteBuf encode(ByteBuf byteBuf, Map<Key, String> tags) {
Assert.notNull(byteBuf, "byteBuf may not be null");
Iterator<Map.Entry<Key, String>> it = tags.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Key, String> entry = it.next();
Key key = entry.getKey();
if (key.wellKnownKey != null) {
byte id = key.wellKnownKey.getIdentifier();
int keyLength = WELL_KNOWN_TAG | id;
byteBuf.writeByte(keyLength);
}
else {
String keyString = key.key;
if (keyString == null) {
continue;
}
int keyLength = ByteBufUtil.utf8Bytes(keyString);
if (keyLength == 0 || keyLength > MAX_TAG_LENGTH) {
continue;
}
byteBuf.writeByte(keyLength);
ByteBufUtil.reserveAndWriteUtf8(byteBuf, keyString, keyLength);
}
boolean hasMoreTags = it.hasNext();
String value = entry.getValue();
int valueLength = ByteBufUtil.utf8Bytes(value);
if (valueLength == 0 || valueLength > MAX_TAG_LENGTH) {
continue;
}
int valueByte;
if (hasMoreTags) {
valueByte = HAS_MORE_TAGS | valueLength;
}
else {
valueByte = valueLength;
}
byteBuf.writeByte(valueByte);
ByteBufUtil.reserveAndWriteUtf8(byteBuf, value, valueLength);
}
return byteBuf;
}
protected static void encodeBigInteger(ByteBuf byteBuf, BigInteger bigInteger) {
byte[] idBytes = bigInteger.toByteArray();
// truncate or pad to 16 bytes or 128 bits
// byte[] normalizedBytes = Arrays.copyOf(idBytes, 16);
byte[] normalizedBytes = new byte[16];
// right shift
int destPos = normalizedBytes.length - idBytes.length;
System.arraycopy(idBytes, 0, normalizedBytes, destPos, idBytes.length);
byteBuf.writeBytes(normalizedBytes);
}
protected static void encodeString(ByteBuf byteBuf, String s) {
int length = NumberUtils.requireUnsignedByte(ByteBufUtil.utf8Bytes(s));
byteBuf.writeByte(length);
ByteBufUtil.reserveAndWriteUtf8(byteBuf, s, length);
}
static TagsMetadata decode(ByteBuf byteBuf) {
AtomicInteger offset = new AtomicInteger(0);
return decode(offset, byteBuf);
}
static TagsMetadata decode(AtomicInteger offset, ByteBuf byteBuf) {
Builder builder = TagsMetadata.builder();
// this means we've reached the end of the buffer
if (offset.get() >= byteBuf.writerIndex()) {
return builder.build();
}
boolean hasMoreTags = true;
while (hasMoreTags) {
int keyByte = byteBuf.getByte(offset.get());
offset.addAndGet(Byte.BYTES);
boolean isWellKnownTag = (keyByte & WELL_KNOWN_TAG) == WELL_KNOWN_TAG;
int keyLength = keyByte & MAX_TAG_LENGTH;
Key key;
if (isWellKnownTag) {
WellKnownKey wellKnownKey = WellKnownKey.fromIdentifier(keyLength);
key = new Key(wellKnownKey, null);
}
else {
String keyString = byteBuf.toString(offset.get(), keyLength,
StandardCharsets.UTF_8);
offset.addAndGet(keyLength);
key = new Key(null, keyString);
}
int valueByte = byteBuf.getByte(offset.get());
offset.addAndGet(Byte.BYTES);
hasMoreTags = (valueByte & HAS_MORE_TAGS) == HAS_MORE_TAGS;
int valueLength = valueByte & MAX_TAG_LENGTH;
String value = byteBuf.toString(offset.get(), valueLength,
StandardCharsets.UTF_8);
offset.addAndGet(valueLength);
builder.with(key, value);
}
return builder.build();
}
protected static BigInteger decodeBigInteger(ByteBuf byteBuf, AtomicInteger offset) {
byte[] idBytes = new byte[16];
byteBuf.getBytes(offset.get(), idBytes, 0, 16);
offset.getAndAdd(16);
return new BigInteger(idBytes);
}
protected static String decodeString(ByteBuf byteBuf, AtomicInteger offset) {
int length = byteBuf.getByte(offset.get());
int index = offset.addAndGet(Byte.BYTES);
String s = byteBuf.toString(index, length, StandardCharsets.UTF_8);
offset.addAndGet(length);
return s;
}
public static class Builder {
private final TagsMetadata metadata;
public Builder() {
this.metadata = new TagsMetadata(new LinkedHashMap<>());
}
public Builder with(String key, String value) {
Assert.notNull(key, "key may not be null");
return with(new Key(key), value);
}
public Builder with(WellKnownKey key, String value) {
Assert.notNull(key, "key may not be null");
return with(new Key(key), value);
}
public Builder with(Key key, String value) {
Assert.notNull(key, "key may not be null");
this.metadata.put(key, value);
return this;
}
public Builder with(TagsMetadata tagsMetadata) {
this.metadata.getTags().putAll(tagsMetadata.getTags());
return this;
}
public TagsMetadata build() {
return this.metadata;
}
public ByteBuf encode() {
return TagsMetadata.encode(build());
}
}
public static class Key {
private final WellKnownKey wellKnownKey;
private final String key;
public Key(WellKnownKey wellKnownKey) {
this(wellKnownKey, null);
}
public Key(String key) {
this(null, key);
}
public Key(WellKnownKey wellKnownKey, String key) {
this.wellKnownKey = wellKnownKey;
this.key = key;
}
public WellKnownKey getWellKnownKey() {
return this.wellKnownKey;
}
public String getKey() {
return this.key;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Key key1 = (Key) o;
return this.wellKnownKey == key1.wellKnownKey
&& Objects.equals(this.key, key1.key);
}
@Override
public int hashCode() {
return Objects.hash(this.wellKnownKey, this.key);
}
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(", ", "[", "]");
if (wellKnownKey != null) {
joiner.add(wellKnownKey.toString());
joiner.add(String.format("0x%02x", wellKnownKey.getIdentifier()));
}
if (key != null) {
joiner.add("'" + key + "'");
}
return joiner.toString();
}
}
}

View File

@@ -0,0 +1,131 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public enum WellKnownKey {
// CHECKSTYLE:OFF
// @formatter:off
UNPARSEABLE_KEY("UNPARSEABLE_KEY_DO_NOT_USE", (byte) -2),
UNKNOWN_RESERVED_KEY("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1),
NO_TAG("NO_TAG_DO_NOT_USE", (byte) 0x00),
SERVICE_NAME("io.rsocket.routing.ServiceName", (byte) 0x01),
ROUTE_ID("io.rsocket.routing.RouteId", (byte) 0x02),
INSTANCE_NAME("io.rsocket.routing.InstanceName", (byte) 0x03),
CLUSTER_NAME("io.rsocket.routing.ClusterName", (byte) 0x04),
PROVIDER("io.rsocket.routing.Provider", (byte) 0x05),
REGION("io.rsocket.routing.Region", (byte) 0x06),
ZONE("io.rsocket.routing.Zone", (byte) 0x07),
DEVICE("io.rsocket.routing.Device", (byte) 0x08),
OS("io.rsocket.routing.OS", (byte) 0x09),
USER_NAME("io.rsocket.routing.UserName", (byte) 0x0A),
USER_ID("io.rsocket.routing.UserId", (byte) 0x0B),
MAJOR_VERSION("io.rsocket.routing.MajorVersion", (byte) 0x0C),
MINOR_VERSION("io.rsocket.routing.MinorVersion", (byte) 0x0D),
PATCH_VERSION("io.rsocket.routing.PatchVersion", (byte) 0x0E),
VERSION("io.rsocket.routing.Version", (byte) 0x0F),
ENVIRONMENT("io.rsocket.routing.Environment", (byte) 0x10),
TESTC_ELL("io.rsocket.routing.TestCell", (byte) 0x11),
DNS("io.rsocket.routing.DNS", (byte) 0x12),
IPV4("io.rsocket.routing.IPv4", (byte) 0x13),
IPV6("io.rsocket.routing.IPv6", (byte) 0x14),
COUNTRY("io.rsocket.routing.Country", (byte) 0x15),
TIME_ZONE("io.rsocket.routing.TimeZone", (byte) 0x1A),
SHARD_KEY("io.rsocket.routing.ShardKey", (byte) 0x1B),
SHARD_METHOD("io.rsocket.routing.ShardMethod", (byte) 0x1C),
STICKY_ROUTE_KEY("io.rsocket.routing.StickyRouteKey", (byte) 0x1D),
LB_METHOD("io.rsocket.routing.LBMethod", (byte) 0x1E),
BROKER_EXTENSION("Broker Implementation Extension Key", (byte) 0x1E),
WELL_KNOWN_EXTENSION("Well Known Extension Key", (byte) 0x1E);
// @formatter:on
// CHECKSTYLE:ON
static final WellKnownKey[] TYPES_BY_ID;
static final Map<String, WellKnownKey> TYPES_BY_STRING;
static {
// precompute an array of all valid mime ids,
// filling the blanks with the RESERVED enum
TYPES_BY_ID = new WellKnownKey[128]; // 0-127 inclusive
Arrays.fill(TYPES_BY_ID, UNKNOWN_RESERVED_KEY);
// also prepare a Map of the types by key string
TYPES_BY_STRING = new HashMap<>(128);
for (WellKnownKey value : values()) {
if (value.getIdentifier() >= 0) {
TYPES_BY_ID[value.getIdentifier()] = value;
TYPES_BY_STRING.put(value.getString(), value);
}
}
}
private final byte identifier;
private final String str;
WellKnownKey(String str, byte identifier) {
this.str = str;
this.identifier = identifier;
}
public static WellKnownKey fromIdentifier(int id) {
if (id < 0x00 || id > 0x7F) {
return UNPARSEABLE_KEY;
}
return TYPES_BY_ID[id];
}
public static WellKnownKey fromString(String mimeType) {
if (mimeType == null) {
throw new IllegalArgumentException("type must be non-null");
}
// force UNPARSEABLE if by chance UNKNOWN_RESERVED_MIME_TYPE's text has been used
if (mimeType.equals(UNKNOWN_RESERVED_KEY.str)) {
return UNPARSEABLE_KEY;
}
return TYPES_BY_STRING.getOrDefault(mimeType, UNPARSEABLE_KEY);
}
/**
* @return the byte identifier of the mime type, guaranteed to be positive or zero.
*/
public byte getIdentifier() {
return identifier;
}
/**
* @return the mime type represented as a {@link String}, which is made of US_ASCII
* compatible characters only
*/
public String getString() {
return str;
}
/** @see #getString() */
@Override
public String toString() {
return str;
}
}

View File

@@ -22,14 +22,15 @@ import org.junit.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration;
import org.springframework.boot.rsocket.server.RSocketServer;
import org.springframework.boot.rsocket.server.RSocketServerBootstrap;
import org.springframework.boot.rsocket.server.RSocketServerFactory;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.cloud.gateway.rsocket.core.GatewayServerRSocketFactoryCustomizer;
import org.springframework.cloud.gateway.rsocket.registry.Registry;
import org.springframework.cloud.gateway.rsocket.registry.RegistryRoutes;
import org.springframework.cloud.gateway.rsocket.registry.RegistrySocketAcceptorFilter;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTable;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTableRoutes;
import org.springframework.cloud.gateway.rsocket.socketacceptor.GatewaySocketAcceptor;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorPredicate;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorPredicateFilter;
@@ -47,11 +48,12 @@ public class GatewayRSocketAutoConfigurationTests {
public void gatewayRSocketConfigured() {
new ReactiveWebApplicationContextRunner().withUserConfiguration(MyConfig.class)
.withConfiguration(
AutoConfigurations.of(GatewayRSocketAutoConfiguration.class,
AutoConfigurations.of(RSocketStrategiesAutoConfiguration.class,
GatewayRSocketAutoConfiguration.class,
CompositeMeterRegistryAutoConfiguration.class,
MetricsAutoConfiguration.class))
.run(context -> assertThat(context).hasSingleBean(Registry.class)
.hasSingleBean(RegistryRoutes.class)
.run(context -> assertThat(context).hasSingleBean(RoutingTable.class)
.hasSingleBean(RoutingTableRoutes.class)
.hasSingleBean(RegistrySocketAcceptorFilter.class)
.hasSingleBean(GatewayServerRSocketFactoryCustomizer.class)
.hasSingleBean(GatewayRSocketProperties.class)

View File

@@ -22,6 +22,7 @@ import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import reactor.core.publisher.Hooks;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
@@ -37,7 +38,8 @@ import org.springframework.util.SocketUtils;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = PingPongApp.class, properties = { "ping.take=10" },
@SpringBootTest(classes = PingPongApp.class,
properties = { "ping.take=10", "ping.subscribe=false" },
webEnvironment = WebEnvironment.RANDOM_PORT)
public class GatewayRSocketIntegrationTests {
@@ -60,6 +62,7 @@ public class GatewayRSocketIntegrationTests {
@BeforeClass
public static void init() {
Hooks.onOperatorDebug();
port = SocketUtils.findAvailableTcpPort();
System.setProperty("spring.rsocket.server.port", String.valueOf(port));
}

View File

@@ -19,12 +19,12 @@ package org.springframework.cloud.gateway.rsocket.core;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.function.Function;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.netty.buffer.Unpooled;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import io.rsocket.util.DefaultPayload;
@@ -36,46 +36,72 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.MonoProcessor;
import reactor.test.StepVerifier;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.springframework.cloud.gateway.rsocket.autoconfigure.GatewayRSocketProperties;
import org.springframework.cloud.gateway.rsocket.registry.LoadBalancedRSocket;
import org.springframework.cloud.gateway.rsocket.registry.LoadBalancedRSocket.EnrichedRSocket;
import org.springframework.cloud.gateway.rsocket.registry.Registry;
import org.springframework.cloud.gateway.rsocket.registry.LoadBalancerFactory;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTable;
import org.springframework.cloud.gateway.rsocket.route.DefaultRoute;
import org.springframework.cloud.gateway.rsocket.route.Route;
import org.springframework.cloud.gateway.rsocket.route.Routes;
import org.springframework.cloud.gateway.rsocket.support.Forwarding;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.cloud.gateway.rsocket.support.RouteSetup;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.cloud.gateway.rsocket.support.WellKnownKey;
import org.springframework.cloud.gateway.rsocket.test.MetadataEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.messaging.rsocket.DefaultMetadataExtractor;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.messaging.rsocket.PayloadUtils;
import org.springframework.messaging.rsocket.RSocketStrategies;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.gateway.rsocket.support.Forwarding.FORWARDING_MIME_TYPE;
/**
* @author Rossen Stoyanchev
* @author Spencer Gibb
*/
public class GatewayRSocketTests {
private static Log logger = LogFactory.getLog(GatewayRSocketTests.class);
private Registry registry;
private RoutingTable routingTable;
private Payload incomingPayload;
private final RSocketStrategies rSocketStrategies = RSocketStrategies.builder()
.decoder(new Forwarding.Decoder()).encoder(new Forwarding.Encoder()).build();
private DefaultMetadataExtractor metadataExtractor = new DefaultMetadataExtractor(
rSocketStrategies.decoders());
// TODO: add tests for metrics and other request types
@Before
public void init() {
registry = mock(Registry.class);
incomingPayload = DefaultPayload.create(Unpooled.EMPTY_BUFFER,
Metadata.from("mock").with("id", "mock1").encode());
routingTable = mock(RoutingTable.class);
this.metadataExtractor.metadataToExtract(FORWARDING_MIME_TYPE, Forwarding.class,
"forwarding");
MetadataEncoder encoder = new MetadataEncoder(Metadata.COMPOSITE_MIME_TYPE,
this.rSocketStrategies);
TagsMetadata tagsMetadata = TagsMetadata.builder()
.with(WellKnownKey.SERVICE_NAME, "mock").build();
Forwarding metadata = new Forwarding(1, tagsMetadata.getTags());
DataBuffer dataBuffer = encoder.metadata(metadata, FORWARDING_MIME_TYPE).encode();
DataBuffer data = MetadataEncoder.emptyDataBuffer(rSocketStrategies);
incomingPayload = PayloadUtils.createPayload(data, dataBuffer);
RSocket rSocket = mock(RSocket.class);
LoadBalancedRSocket loadBalancedRSocket = mock(LoadBalancedRSocket.class);
when(registry.getRegistered(any(Metadata.class))).thenReturn(loadBalancedRSocket);
Mono<EnrichedRSocket> mono = Mono
.just(new EnrichedRSocket(rSocket, getMetadata()));
when(loadBalancedRSocket.choose()).thenReturn(mono);
Tuple2<String, RSocket> tuple = Tuples.of("1111", rSocket);
when(routingTable.findRSockets(any(TagsMetadata.class)))
.thenReturn(Collections.singletonList(tuple));
when(rSocket.requestResponse(any(Payload.class)))
.thenReturn(Mono.just(DefaultPayload.create("response")));
@@ -87,8 +113,8 @@ public class GatewayRSocketTests {
TestFilter filter2 = new TestFilter();
TestFilter filter3 = new TestFilter();
Payload payload = new TestGatewayRSocket(registry,
new TestRoutes(filter1, filter2, filter3))
Payload payload = new TestGatewayRSocket(routingTable,
new TestRoutes(filter1, filter2, filter3), metadataExtractor)
.requestResponse(incomingPayload).block(Duration.ZERO);
assertThat(filter1.invoked()).isTrue();
@@ -99,8 +125,8 @@ public class GatewayRSocketTests {
@Test
public void zeroFilters() {
Payload payload = new TestGatewayRSocket(registry, new TestRoutes())
.requestResponse(incomingPayload).block(Duration.ZERO);
Payload payload = new TestGatewayRSocket(routingTable, new TestRoutes(),
metadataExtractor).requestResponse(incomingPayload).block(Duration.ZERO);
assertThat(payload).isNotNull();
}
@@ -112,13 +138,13 @@ public class GatewayRSocketTests {
ShortcircuitingFilter filter2 = new ShortcircuitingFilter();
TestFilter filter3 = new TestFilter();
TestGatewayRSocket gatewayRSocket = new TestGatewayRSocket(registry,
new TestRoutes(filter1, filter2, filter3));
TestGatewayRSocket gatewayRSocket = new TestGatewayRSocket(routingTable,
new TestRoutes(filter1, filter2, filter3), metadataExtractor);
Mono<Payload> response = gatewayRSocket.requestResponse(incomingPayload);
// a false filter will create a pending rsocket that blocks forever
// this tweaks the rsocket to compelte.
gatewayRSocket.processor.onNext(null);
// this tweaks the rsocket to complete.
gatewayRSocket.getProcessor().onNext(null);
StepVerifier.withVirtualTime(() -> response).expectSubscription()
.verifyComplete();
@@ -133,8 +159,9 @@ public class GatewayRSocketTests {
AsyncFilter filter = new AsyncFilter();
Payload payload = new TestGatewayRSocket(registry, new TestRoutes(filter))
.requestResponse(incomingPayload).block(Duration.ofSeconds(5));
Payload payload = new TestGatewayRSocket(routingTable, new TestRoutes(filter),
metadataExtractor).requestResponse(incomingPayload)
.block(Duration.ofSeconds(5));
assertThat(filter.invoked()).isTrue();
assertThat(payload).isNotNull();
@@ -146,37 +173,55 @@ public class GatewayRSocketTests {
ExceptionFilter filter = new ExceptionFilter();
new TestGatewayRSocket(registry, new TestRoutes(filter))
new TestGatewayRSocket(routingTable, new TestRoutes(filter), metadataExtractor)
.requestResponse(incomingPayload).block(Duration.ofSeconds(5));
// assertNull(socket);
}
private static Metadata getMetadata() {
return Metadata.from("service").with("id", "service1").build();
private static RouteSetup getMetadata() {
return new RouteSetup(1L, "service", new LinkedHashMap<>());
}
private static class TestGatewayRSocket extends GatewayRSocket {
TestGatewayRSocket(RoutingTable routingTable, Routes routes,
MetadataExtractor metadataExtractor) {
super(routes, new TestPendingFactory(routingTable, routes, metadataExtractor),
new LoadBalancerFactory(routingTable), new SimpleMeterRegistry(),
new GatewayRSocketProperties(), metadataExtractor, getMetadata());
}
private MonoProcessor<RSocket> getProcessor() {
TestPendingFactory factory = (TestPendingFactory) super.getPendingFactory();
return factory.processor;
}
}
private static class TestPendingFactory extends PendingRequestRSocketFactory {
private final MonoProcessor<RSocket> processor = MonoProcessor.create();
TestGatewayRSocket(Registry registry, Routes routes) {
super(registry, routes, new SimpleMeterRegistry(),
new GatewayRSocketProperties(), getMetadata());
private final MetadataExtractor metadataExtractor;
TestPendingFactory(RoutingTable routingTable, Routes routes,
MetadataExtractor metadataExtractor) {
super(routingTable, routes, metadataExtractor);
this.metadataExtractor = metadataExtractor;
}
@Override
PendingRequestRSocket constructPendingRSocket(GatewayExchange exchange) {
Function<Registry.RegisteredEvent, Mono<Route>> routeFinder = registeredEvent -> getRouteMono(
protected PendingRequestRSocket constructPendingRSocket(
GatewayExchange exchange) {
Function<RoutingTable.RegisteredEvent, Mono<Route>> routeFinder = registeredEvent -> getRouteMono(
registeredEvent, exchange);
return new PendingRequestRSocket(routeFinder, map -> {
Tags tags = exchange.getTags().and("responder.id", map.get("id"));
exchange.setTags(tags);
}, processor);
}
public MonoProcessor<RSocket> getProcessor() {
return processor;
return new PendingRequestRSocket(metadataExtractor, routeFinder,
tagsMetadata -> {
Tags tags = exchange.getTags().and("responder.id",
tagsMetadata.getRouteId());
exchange.setTags(tags);
}, processor);
}
}
@@ -197,8 +242,8 @@ public class GatewayRSocketTests {
TestRoutes(List<GatewayFilter> filters) {
this.filters = filters;
route = Route.builder().id("route1")
.routingMetadata(Metadata.from("mock").build())
route = DefaultRoute.builder().id("route1")
.routingMetadata(new RouteSetup(1L, "mock", new LinkedHashMap<>()))
.predicate(exchange -> Mono.just(true)).filters(filters).build();
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.registry;
import java.util.HashMap;
import java.util.HashSet;
import io.rsocket.RSocket;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.cloud.gateway.rsocket.core.GatewayExchange;
import org.springframework.cloud.gateway.rsocket.route.Route;
import org.springframework.cloud.gateway.rsocket.support.Forwarding;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.cloud.gateway.rsocket.support.WellKnownKey;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.gateway.rsocket.core.GatewayExchange.Type.REQUEST_RESPONSE;
public class RoutingTableRoutesTests {
@Test
public void routesAreBuilt() {
RoutingTable routingTable = mock(RoutingTable.class);
RoutingTableRoutes routes = new RoutingTableRoutes(routingTable);
HashSet<String> routeIds = new HashSet<>();
routeIds.add("2");
when(routingTable.findRouteIds(any(TagsMetadata.class))).thenReturn(routeIds);
addRoute(routes, "1");
addRoute(routes, "2");
addRoute(routes, "3");
HashMap<TagsMetadata.Key, String> tags = new HashMap<>();
tags.put(new TagsMetadata.Key(WellKnownKey.ROUTE_ID), "2");
Forwarding forwarding = new Forwarding(1L, tags);
Mono<Route> routeMono = routes
.findRoute(new GatewayExchange(REQUEST_RESPONSE, forwarding));
StepVerifier.create(routeMono).consumeNextWith(route -> {
assertThat(route).isNotNull().extracting(Route::getId).isEqualTo("2");
}).verifyComplete();
}
void addRoute(RoutingTableRoutes routes, String routeId) {
// @formatter:off
TagsMetadata tagsMetadata = TagsMetadata.builder()
.with(WellKnownKey.ROUTE_ID, routeId)
.build();
// @formatter:on
routes.accept(
new RoutingTable.RegisteredEvent(tagsMetadata, mock(RSocket.class)));
}
}

View File

@@ -0,0 +1,170 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.registry;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import io.rsocket.AbstractRSocket;
import io.rsocket.RSocket;
import org.junit.Test;
import org.roaringbitmap.RoaringBitmap;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.cloud.gateway.rsocket.support.WellKnownKey;
import org.springframework.core.style.ToStringCreator;
import static org.assertj.core.api.Assertions.assertThat;
public class RoutingTableTests {
@Test
public void testIndexesCreatedAndSearchWorks() {
RoutingTable routingTable = new RoutingTable();
// @formatter:off
TagsMetadata setupTags1 = TagsMetadata.builder()
.with(WellKnownKey.ROUTE_ID, "1111")
.with(WellKnownKey.SERVICE_NAME, "serviceA")
.with(WellKnownKey.CLUSTER_NAME, "clusterA")
.build();
TagsMetadata setupTags2 = TagsMetadata.builder()
.with(WellKnownKey.ROUTE_ID, "2222")
.with(WellKnownKey.SERVICE_NAME, "serviceA")
.with(WellKnownKey.CLUSTER_NAME, "clusterB")
.build();
TagsMetadata setupTags3 = TagsMetadata.builder()
.with(WellKnownKey.ROUTE_ID, "3333")
.with(WellKnownKey.SERVICE_NAME, "serviceB")
.with(WellKnownKey.REGION, "region1")
.build();
TagsMetadata setupTags4 = TagsMetadata.builder()
.with(WellKnownKey.ROUTE_ID, "4444")
.with(WellKnownKey.SERVICE_NAME, "serviceB")
.with(WellKnownKey.CLUSTER_NAME, "clusterB")
.with(WellKnownKey.ZONE, "zone1")
.build();
TagsMetadata setupTags5 = TagsMetadata.builder()
.with(WellKnownKey.ROUTE_ID, "5555")
.with(WellKnownKey.SERVICE_NAME, "serviceA")
.with(WellKnownKey.CLUSTER_NAME, "clusterB")
.build();
// @formatter:on
AtomicInteger internalRouteId = routingTable.internalRouteId;
RSocket rSocket1 = assertRegister(routingTable, setupTags1,
internalRouteId.get() + 1);
internalRouteId.set(99);
RSocket rSocket2 = assertRegister(routingTable, setupTags2,
internalRouteId.get() + 1);
internalRouteId.set(999);
RSocket rSocket3 = assertRegister(routingTable, setupTags3,
internalRouteId.get() + 1);
internalRouteId.set(9999);
RSocket rSocket4 = assertRegister(routingTable, setupTags4,
internalRouteId.get() + 1);
internalRouteId.set(99999);
RSocket rSocket5 = assertRegister(routingTable, setupTags5,
internalRouteId.get() + 1);
// @formatter:off
TagsMetadata searchTags1 = TagsMetadata.builder()
.with(WellKnownKey.SERVICE_NAME, "serviceA")
.with(WellKnownKey.CLUSTER_NAME, "clusterB")
.build();
// @formatter:on
List<Tuple2<String, RSocket>> results1 = routingTable.findRSockets(searchTags1);
assertThat(results1).containsOnly(Tuples.of("2222", rSocket2),
Tuples.of("5555", rSocket5));
// @formatter:off
TagsMetadata searchTags2 = TagsMetadata.builder()
.with(WellKnownKey.ROUTE_ID, "3333")
.build();
// @formatter:on
List<Tuple2<String, RSocket>> results2 = routingTable.findRSockets(searchTags2);
assertThat(results2).containsOnly(Tuples.of("3333", rSocket3));
// @formatter:off
TagsMetadata searchTags3 = TagsMetadata.builder()
.with(WellKnownKey.ZONE, "zone1")
.with(WellKnownKey.SERVICE_NAME, "serviceB")
.with(WellKnownKey.CLUSTER_NAME, "clusterB")
.build();
// @formatter:on
List<Tuple2<String, RSocket>> results3 = routingTable.findRSockets(searchTags3);
assertThat(results3).containsOnly(Tuples.of("4444", rSocket4));
assertDeregister(routingTable, setupTags1);
assertDeregister(routingTable, setupTags2);
assertDeregister(routingTable, setupTags3);
assertDeregister(routingTable, setupTags4);
assertDeregister(routingTable, setupTags5);
assertThat(routingTable.deregister(setupTags5)).isFalse();
}
void assertDeregister(RoutingTable routingTable, TagsMetadata tagsMetadata) {
boolean result = routingTable.deregister(tagsMetadata);
assertThat(result).isTrue();
String routeId = tagsMetadata.getRouteId();
assertThat(routingTable.internalRouteIdToRouteId).doesNotContainValue(routeId);
assertThat(routingTable.routeIdToRSocket).doesNotContainKey(routeId);
assertThat(routingTable.routeIdToTags).doesNotContainKey(routeId);
}
private RSocket assertRegister(RoutingTable routingTable, TagsMetadata tagsMetadata,
int internalId) {
String routeId = tagsMetadata.getRouteId();
RSocket rsocket = new TestRSocket(routeId);
routingTable.register(tagsMetadata, rsocket);
assertThat(routingTable.internalRouteId).hasValue(internalId);
assertThat(routingTable.internalRouteIdToRouteId).containsEntry(internalId,
routeId);
assertThat(routingTable.routeIdToRSocket).containsKey(routeId);
tagsMetadata.getTags().forEach((key, value) -> {
RoutingTable.RegistryKey registryKey = new RoutingTable.RegistryKey(key,
value);
assertThat(routingTable.tagsToBitmaps).containsKey(registryKey);
RoaringBitmap bitmap = routingTable.tagsToBitmaps.get(registryKey);
assertThat(bitmap.contains(internalId));
});
return rsocket;
}
static class TestRSocket extends AbstractRSocket {
final String routeId;
TestRSocket(String routeId) {
this.routeId = routeId;
}
@Override
public String toString() {
return new ToStringCreator(this).append("routeId", routeId).toString();
}
}
}

View File

@@ -19,10 +19,12 @@ package org.springframework.cloud.gateway.rsocket.socketacceptor;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.rsocket.ConnectionSetupPayload;
import io.rsocket.Payload;
import io.rsocket.RSocket;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -32,22 +34,31 @@ import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.rsocket.autoconfigure.GatewayRSocketProperties;
import org.springframework.cloud.gateway.rsocket.core.GatewayRSocket;
import org.springframework.cloud.gateway.rsocket.core.GatewayRSocketFactory;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.cloud.gateway.rsocket.support.RouteSetup;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.cloud.gateway.rsocket.test.MetadataEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.messaging.rsocket.DefaultMetadataExtractor;
import org.springframework.messaging.rsocket.PayloadUtils;
import org.springframework.messaging.rsocket.RSocketStrategies;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.gateway.rsocket.support.RouteSetup.ROUTE_SETUP_MIME_TYPE;
/**
* @author Rossen Stoyanchev
* @author Spencer Gibb
*/
public class GatewaySocketAcceptorTests {
private static Log logger = LogFactory.getLog(GatewaySocketAcceptorTests.class);
private GatewayRSocket.Factory factory;
private GatewayRSocketFactory factory;
private ConnectionSetupPayload setupPayload;
@@ -57,15 +68,38 @@ public class GatewaySocketAcceptorTests {
private GatewayRSocketProperties properties = new GatewayRSocketProperties();
private final RSocketStrategies rSocketStrategies = RSocketStrategies.builder()
.decoder(new RouteSetup.Decoder()).encoder(new RouteSetup.Encoder()).build();
private DefaultMetadataExtractor metadataExtractor = new DefaultMetadataExtractor(
rSocketStrategies.decoders());
@Before
public void init() {
this.factory = mock(GatewayRSocket.Factory.class);
this.factory = mock(GatewayRSocketFactory.class);
this.setupPayload = mock(ConnectionSetupPayload.class);
this.sendingSocket = mock(RSocket.class);
this.meterRegistry = new SimpleMeterRegistry();
when(this.factory.create(any(Metadata.class)))
this.metadataExtractor.metadataToExtract(ROUTE_SETUP_MIME_TYPE, RouteSetup.class,
"routesetup");
when(this.factory.create(any(TagsMetadata.class)))
.thenReturn(mock(GatewayRSocket.class));
when(this.setupPayload.metadataMimeType())
.thenReturn(Metadata.COMPOSITE_MIME_TYPE.toString());
when(this.setupPayload.hasMetadata()).thenReturn(true);
MetadataEncoder encoder = new MetadataEncoder(Metadata.COMPOSITE_MIME_TYPE,
this.rSocketStrategies);
encoder.metadata(new RouteSetup(1, "myservice", new LinkedHashMap<>()),
ROUTE_SETUP_MIME_TYPE);
DataBuffer dataBuffer = encoder.encode();
DataBuffer data = MetadataEncoder.emptyDataBuffer(rSocketStrategies);
Payload payload = PayloadUtils.createPayload(data, dataBuffer);
when(setupPayload.metadata()).thenReturn(payload.metadata());
}
// TODO: test metrics
@@ -78,7 +112,8 @@ public class GatewaySocketAcceptorTests {
RSocket socket = new GatewaySocketAcceptor(this.factory,
Arrays.asList(filter1, filter2, filter3), this.meterRegistry,
this.properties).accept(this.setupPayload, this.sendingSocket)
this.properties, this.metadataExtractor)
.accept(this.setupPayload, this.sendingSocket)
.block(Duration.ZERO);
assertThat(filter1.invoked()).isTrue();
@@ -90,7 +125,7 @@ public class GatewaySocketAcceptorTests {
@Test
public void zeroFilters() {
RSocket socket = new GatewaySocketAcceptor(this.factory, Collections.emptyList(),
this.meterRegistry, this.properties)
this.meterRegistry, this.properties, this.metadataExtractor)
.accept(this.setupPayload, this.sendingSocket)
.block(Duration.ZERO);
@@ -106,7 +141,8 @@ public class GatewaySocketAcceptorTests {
RSocket socket = new GatewaySocketAcceptor(this.factory,
Arrays.asList(filter1, filter2, filter3), this.meterRegistry,
this.properties).accept(this.setupPayload, this.sendingSocket)
this.properties, this.metadataExtractor)
.accept(this.setupPayload, this.sendingSocket)
.block(Duration.ZERO);
assertThat(filter1.invoked()).isTrue();
@@ -121,7 +157,7 @@ public class GatewaySocketAcceptorTests {
AsyncFilter filter = new AsyncFilter();
RSocket socket = new GatewaySocketAcceptor(this.factory, singletonList(filter),
this.meterRegistry, this.properties)
this.meterRegistry, this.properties, this.metadataExtractor)
.accept(this.setupPayload, this.sendingSocket)
.block(Duration.ofSeconds(5));
@@ -136,7 +172,8 @@ public class GatewaySocketAcceptorTests {
ExceptionFilter filter = new ExceptionFilter();
new GatewaySocketAcceptor(this.factory, singletonList(filter), this.meterRegistry,
this.properties).accept(this.setupPayload, this.sendingSocket)
this.properties, this.metadataExtractor)
.accept(this.setupPayload, this.sendingSocket)
.block(Duration.ofSeconds(5));
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.rsocket.Payload;
import io.rsocket.util.DefaultPayload;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.rsocket.test.MetadataEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = "spring.rsocket.server.port=0", webEnvironment = RANDOM_PORT)
public class ForwardingIntegrationTests extends ForwardingTests {
@Autowired
private RSocketStrategies strategies;
@Override
protected ByteBuf encode(Forwarding forwarding) {
DataBuffer dataBuffer = new MetadataEncoder(Metadata.COMPOSITE_MIME_TYPE,
strategies).metadata(forwarding, Forwarding.FORWARDING_MIME_TYPE)
.encode();
return TagsMetadata.asByteBuf(dataBuffer);
}
@Override
protected Forwarding decode(ByteBuf byteBuf) {
MetadataExtractor metadataExtractor = strategies.metadataExtractor();
Payload payload = DefaultPayload.create(Unpooled.EMPTY_BUFFER, byteBuf);
Map<String, Object> metadata = metadataExtractor.extract(payload,
Metadata.COMPOSITE_MIME_TYPE);
assertThat(metadata).containsKey("forwarding");
return (Forwarding) metadata.get("forwarding");
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class Config {
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import io.netty.buffer.ByteBuf;
import org.junit.Test;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata.Key;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.rsocket.support.RouteSetupTests.MAX_BIGINT;
import static org.springframework.cloud.gateway.rsocket.support.RouteSetupTests.TWO_BYTE_BIGINT;
import static org.springframework.cloud.gateway.rsocket.support.WellKnownKey.REGION;
public class ForwardingTests {
@Test
public void encodeAndDecodeWorksMaxBigint() {
ByteBuf byteBuf = createForwarding(MAX_BIGINT);
assertForwarding(byteBuf, MAX_BIGINT);
}
@Test
public void encodeAndDecodeWorksMinBigint() {
ByteBuf byteBuf = createForwarding(BigInteger.ONE);
assertForwarding(byteBuf, BigInteger.ONE);
}
@Test
public void encodeAndDecodeWorksTwoBytes() {
ByteBuf byteBuf = createForwarding(TWO_BYTE_BIGINT);
assertForwarding(byteBuf, TWO_BYTE_BIGINT);
}
protected ByteBuf createForwarding(BigInteger originRouteId) {
LinkedHashMap<Key, String> tags = new LinkedHashMap<>();
tags.put(new Key(REGION), "us-east-1");
Forwarding forwarding = new Forwarding(originRouteId, tags);
return encode(forwarding);
}
protected ByteBuf encode(Forwarding forwarding) {
return forwarding.encode();
}
protected void assertForwarding(ByteBuf byteBuf, BigInteger originRouteId) {
Forwarding forwarding = decode(byteBuf);
assertThat(forwarding).isNotNull();
assertThat(forwarding.getOriginRouteId()).isEqualTo(originRouteId);
assertThat(forwarding.getTags()).hasSize(1).containsOnlyKeys(new Key(REGION))
.containsValues("us-east-1");
}
protected Forwarding decode(ByteBuf byteBuf) {
return Forwarding.decode(byteBuf);
}
}

View File

@@ -1,85 +0,0 @@
/*
* Copyright 2018-2019 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.gateway.rsocket.support;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;
import io.netty.buffer.ByteBuf;
import org.junit.Test;
import org.springframework.util.Assert;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.rsocket.support.Metadata.matches;
public class MetadataTests {
@Test
public void encodeAndDecodeJustName() {
ByteBuf byteBuf = Metadata.from("test").encode();
assertMetadata(byteBuf, "test");
}
@Test
public void encodeAndDecodeWorks() {
ByteBuf byteBuf = Metadata.from("test1").with("key1111", "val111111")
.with("key22", "val222").encode();
Metadata metadata = assertMetadata(byteBuf, "test1");
Map<String, String> properties = metadata.getProperties();
assertThat(properties).hasSize(2).containsOnlyKeys("key1111", "key22")
.containsValues("val111111", "val222");
}
private Metadata assertMetadata(ByteBuf byteBuf, String name) {
Metadata metadata = Metadata.decodeMetadata(byteBuf);
assertThat(metadata).isNotNull();
assertThat(metadata.getName()).isEqualTo(name);
return metadata;
}
@Test
public void nullMetadataDoesNotMatch() {
assertThat(matches(null, new HashMap<>())).isFalse();
assertThat(matches(new HashMap<>(), null)).isFalse();
}
@Test
public void metadataSubsetMatches() {
assertThat(matches(metadata(2), metadata(3))).isTrue();
}
@Test
public void metadataEqualSetMatches() {
assertThat(matches(metadata(3), metadata(3))).isTrue();
}
@Test
public void metadataSuperSetDoesNotMatch() {
assertThat(matches(metadata(3), metadata(2))).isFalse();
}
private Map<String, String> metadata(int size) {
Assert.isTrue(size > 0, "size must be > 0");
HashMap<String, String> metadata = new HashMap<>();
IntStream.rangeClosed(1, size).forEach(i -> metadata.put("key" + i, "val" + i));
return metadata;
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.rsocket.Payload;
import io.rsocket.util.DefaultPayload;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.rsocket.test.MetadataEncoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = "spring.rsocket.server.port=0", webEnvironment = RANDOM_PORT)
public class RouteSetupIntegrationTests extends RouteSetupTests {
@Autowired
private RSocketStrategies strategies;
@Override
protected ByteBuf encode(RouteSetup routeSetup) {
DataBuffer dataBuffer = new MetadataEncoder(Metadata.COMPOSITE_MIME_TYPE,
strategies).metadata(routeSetup, RouteSetup.ROUTE_SETUP_MIME_TYPE)
.encode();
return TagsMetadata.asByteBuf(dataBuffer);
}
@Override
protected RouteSetup decode(ByteBuf byteBuf) {
MetadataExtractor metadataExtractor = strategies.metadataExtractor();
Payload payload = DefaultPayload.create(Unpooled.EMPTY_BUFFER, byteBuf);
Map<String, Object> metadata = metadataExtractor.extract(payload,
Metadata.COMPOSITE_MIME_TYPE);
assertThat(metadata).containsKey("routesetup");
return (RouteSetup) metadata.get("routesetup");
}
@SpringBootConfiguration
@EnableAutoConfiguration
static class Config {
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import io.netty.buffer.ByteBuf;
import org.junit.Test;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata.Key;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.rsocket.support.WellKnownKey.REGION;
public class RouteSetupTests {
static final BigInteger MAX_BIGINT = new BigInteger(
"170141183460469231731687303715884105727");
static final BigInteger TWO_BYTE_BIGINT = new BigInteger("128");
@Test
public void bigIntegerTest() {
byte[] bytes = MAX_BIGINT.toByteArray();
System.out.println("max bytes: " + bytes.length);
bytes = BigInteger.ONE.toByteArray();
System.out.println("min bytes: " + bytes.length);
BigInteger bigInteger = TWO_BYTE_BIGINT;
bytes = bigInteger.toByteArray();
System.out.println("16 bytes: " + bytes.length);
}
@Test
public void encodeAndDecodeWorksMaxBigint() {
ByteBuf byteBuf = createRouteSetup(MAX_BIGINT);
assertRouteSetup(byteBuf, MAX_BIGINT);
}
@Test
public void encodeAndDecodeWorksMinBigint() {
ByteBuf byteBuf = createRouteSetup(BigInteger.ONE);
assertRouteSetup(byteBuf, BigInteger.ONE);
}
@Test
public void encodeAndDecodeWorksTwoBytes() {
ByteBuf byteBuf = createRouteSetup(TWO_BYTE_BIGINT);
assertRouteSetup(byteBuf, TWO_BYTE_BIGINT);
}
@Test
public void encodeAndDecodeWorksEmptyTags() {
ByteBuf byteBuf = createRouteSetup(TWO_BYTE_BIGINT, false);
assertRouteSetup(byteBuf, TWO_BYTE_BIGINT, false);
}
protected ByteBuf createRouteSetup(BigInteger id) {
return createRouteSetup(id, true);
}
protected ByteBuf createRouteSetup(BigInteger id, boolean addTags) {
LinkedHashMap<Key, String> tags = new LinkedHashMap<>();
if (addTags) {
tags.put(new Key(REGION), "us-east-1");
}
RouteSetup routeSetup = new RouteSetup(id, "myservice11111111", tags);
return encode(routeSetup);
}
protected ByteBuf encode(RouteSetup routeSetup) {
return routeSetup.encode();
}
protected void assertRouteSetup(ByteBuf byteBuf, BigInteger routeId) {
assertRouteSetup(byteBuf, routeId, true);
}
protected void assertRouteSetup(ByteBuf byteBuf, BigInteger routeId,
boolean addTags) {
RouteSetup routeSetup = decode(byteBuf);
assertThat(routeSetup).isNotNull();
assertThat(routeSetup.getId()).isEqualTo(routeId);
assertThat(routeSetup.getServiceName()).isEqualTo("myservice11111111");
if (addTags) {
assertThat(routeSetup.getTags()).hasSize(1).containsOnlyKeys(new Key(REGION))
.containsValues("us-east-1");
}
else {
assertThat(routeSetup.getTags()).isEmpty();
}
}
protected RouteSetup decode(ByteBuf byteBuf) {
return RouteSetup.decode(byteBuf);
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.support;
import io.netty.buffer.ByteBuf;
import org.junit.Test;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata.Key;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.rsocket.support.WellKnownKey.ROUTE_ID;
import static org.springframework.cloud.gateway.rsocket.support.WellKnownKey.SERVICE_NAME;
public class TagsMetadataTests {
@Test
public void encodeAndDecodeWorksAllWellKnowKeys() {
ByteBuf byteBuf = TagsMetadata.builder().with(ROUTE_ID, "routeId1111111")
.with(SERVICE_NAME, "serviceName2222222").encode();
TagsMetadata metadata = TagsMetadata.decode(byteBuf);
assertThat(metadata).isNotNull();
assertThat(metadata.getTags()).hasSize(2)
.containsOnlyKeys(new Key(ROUTE_ID), new Key(SERVICE_NAME))
.containsValues("routeId1111111", "serviceName2222222");
}
@Test
public void encodeAndDecodeWorksAllStringKeys() {
ByteBuf byteBuf = TagsMetadata.builder().with("mykey111111111", "myval1111111")
.with("mykey2222222222", "myval2222222").encode();
TagsMetadata metadata = TagsMetadata.decode(byteBuf);
assertThat(metadata).isNotNull();
assertThat(metadata.getTags()).hasSize(2)
.containsOnlyKeys(new Key("mykey111111111"), new Key("mykey2222222222"))
.containsValues("myval1111111", "myval2222222");
}
@Test
public void encodeAndDecodeWorksMixedKeys() {
ByteBuf byteBuf = TagsMetadata.builder().with(ROUTE_ID, "routeId1111111")
.with("mykey2222222222", "myval2222222").encode();
TagsMetadata metadata = TagsMetadata.decode(byteBuf);
assertThat(metadata).isNotNull();
assertThat(metadata.getTags()).hasSize(2)
.containsOnlyKeys(new Key(ROUTE_ID), new Key("mykey2222222222"))
.containsValues("routeId1111111", "myval2222222");
}
}

View File

@@ -0,0 +1,256 @@
/*
* Copyright 2013-2019 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.gateway.rsocket.test;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.rsocket.metadata.CompositeMetadataFlyweight;
import io.rsocket.metadata.TaggingMetadataFlyweight;
import io.rsocket.metadata.WellKnownMimeType;
import org.springframework.core.ResolvableType;
import org.springframework.core.codec.Encoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.NettyDataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.lang.Nullable;
import org.springframework.messaging.rsocket.MetadataExtractor;
import org.springframework.messaging.rsocket.PayloadUtils;
import org.springframework.messaging.rsocket.RSocketRequester;
import org.springframework.messaging.rsocket.RSocketStrategies;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MimeType;
import org.springframework.util.ObjectUtils;
/**
* Helps to collect metadata values and mime types, and encode them. TODO: remove if
* framework class is made public.
*
* {@link MetadataExtractor} original.
*
* @author Rossen Stoyanchev
*/
public class MetadataEncoder {
/** For route variable replacement. */
private static final Pattern VARS_PATTERN = Pattern.compile("\\{([^/]+?)}");
private final MimeType metadataMimeType;
private final RSocketStrategies strategies;
private final boolean isComposite;
private final ByteBufAllocator allocator;
@Nullable
private String route;
private final Map<Object, MimeType> metadata = new LinkedHashMap<>(4);
public MetadataEncoder(MimeType metadataMimeType, RSocketStrategies strategies) {
Assert.notNull(metadataMimeType, "'metadataMimeType' is required");
Assert.notNull(strategies, "RSocketStrategies is required");
this.metadataMimeType = metadataMimeType;
this.strategies = strategies;
this.isComposite = this.metadataMimeType.toString()
.equals(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString());
this.allocator = bufferFactory() instanceof NettyDataBufferFactory
? ((NettyDataBufferFactory) bufferFactory()).getByteBufAllocator()
: ByteBufAllocator.DEFAULT;
}
private DataBufferFactory bufferFactory() {
return this.strategies.dataBufferFactory();
}
/**
* Set the route to a remote handler as described in
* {@link RSocketRequester#route(String, Object...)}.
*/
public MetadataEncoder route(String route, Object... routeVars) {
this.route = expand(route, routeVars);
assertMetadataEntryCount();
return this;
}
private static String expand(String route, Object... routeVars) {
if (ObjectUtils.isEmpty(routeVars)) {
return route;
}
StringBuffer sb = new StringBuffer();
int index = 0;
Matcher matcher = VARS_PATTERN.matcher(route);
while (matcher.find()) {
Assert.isTrue(index < routeVars.length,
() -> "No value for variable '" + matcher.group(1) + "'");
String value = routeVars[index].toString();
value = value.contains(".") ? value.replaceAll("\\.", "%2E") : value;
matcher.appendReplacement(sb, value);
index++;
}
return sb.toString();
}
private void assertMetadataEntryCount() {
if (!this.isComposite) {
int count = this.route != null ? this.metadata.size() + 1
: this.metadata.size();
Assert.isTrue(count < 2,
"Composite metadata required for multiple metadata entries.");
}
}
/**
* Add a metadata entry. If called more than once or in addition to route, composite
* metadata must be in use.
*/
public MetadataEncoder metadata(Object metadata, @Nullable MimeType mimeType) {
if (this.isComposite) {
Assert.notNull(mimeType,
"MimeType is required for composite metadata entries.");
}
else if (mimeType == null) {
mimeType = this.metadataMimeType;
}
else if (!this.metadataMimeType.equals(mimeType)) {
throw new IllegalArgumentException("Mime type is optional (may be null) "
+ "but was provided and does not match the connection metadata mime type.");
}
this.metadata.put(metadata, mimeType);
assertMetadataEntryCount();
return this;
}
/**
* Add route and/or metadata, both optional.
*/
public MetadataEncoder metadataAndOrRoute(@Nullable Map<Object, MimeType> metadata,
@Nullable String route, @Nullable Object[] vars) {
if (route != null) {
this.route = expand(route, vars != null ? vars : new Object[0]);
}
if (!CollectionUtils.isEmpty(metadata)) {
for (Map.Entry<Object, MimeType> entry : metadata.entrySet()) {
metadata(entry.getKey(), entry.getValue());
}
}
assertMetadataEntryCount();
return this;
}
/**
* Encode the collected metadata entries to a {@code DataBuffer}.
* @see PayloadUtils#createPayload(DataBuffer, DataBuffer)
*/
public DataBuffer encode() {
if (this.isComposite) {
CompositeByteBuf composite = this.allocator.compositeBuffer();
try {
if (this.route != null) {
CompositeMetadataFlyweight.encodeAndAddMetadata(composite,
this.allocator, WellKnownMimeType.MESSAGE_RSOCKET_ROUTING,
encodeRoute());
}
this.metadata.forEach((value, mimeType) -> {
ByteBuf metadata = (value instanceof ByteBuf ? (ByteBuf) value
: asByteBuf(encodeEntry(value, mimeType)));
CompositeMetadataFlyweight.encodeAndAddMetadata(composite,
this.allocator, mimeType.toString(), metadata);
});
return asDataBuffer(composite);
}
catch (Throwable ex) {
composite.release();
throw ex;
}
}
else if (this.route != null) {
Assert.isTrue(this.metadata.isEmpty(),
"Composite metadata required for route and other entries");
String routingMimeType = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING
.getString();
return this.metadataMimeType.toString().equals(routingMimeType)
? asDataBuffer(encodeRoute())
: encodeEntry(this.route, this.metadataMimeType);
}
else {
Assert.isTrue(this.metadata.size() == 1,
"Composite metadata required for multiple entries");
Map.Entry<Object, MimeType> entry = this.metadata.entrySet().iterator()
.next();
if (!this.metadataMimeType.equals(entry.getValue())) {
throw new IllegalArgumentException(
"Connection configured for metadata mime type " + "'"
+ this.metadataMimeType + "', but actual is `"
+ this.metadata + "`");
}
return encodeEntry(entry.getKey(), entry.getValue());
}
}
private ByteBuf encodeRoute() {
return TaggingMetadataFlyweight.createRoutingMetadata(this.allocator,
Collections.singletonList(this.route)).getContent();
}
@SuppressWarnings("unchecked")
private <T> DataBuffer encodeEntry(Object metadata, MimeType mimeType) {
if (metadata instanceof ByteBuf) {
return asDataBuffer((ByteBuf) metadata);
}
ResolvableType type = ResolvableType.forInstance(metadata);
Encoder<T> encoder = this.strategies.encoder(type, mimeType);
Assert.notNull(encoder, () -> "No encoder for metadata " + metadata
+ ", mimeType '" + mimeType + "'");
return encoder.encodeValue((T) metadata, bufferFactory(), type, mimeType,
Collections.emptyMap());
}
private DataBuffer asDataBuffer(ByteBuf byteBuf) {
if (bufferFactory() instanceof NettyDataBufferFactory) {
return ((NettyDataBufferFactory) bufferFactory()).wrap(byteBuf);
}
else {
DataBuffer buffer = bufferFactory().wrap(byteBuf.nioBuffer());
byteBuf.release();
return buffer;
}
}
public static DataBuffer emptyDataBuffer(RSocketStrategies rSocketStrategies) {
return rSocketStrategies.dataBufferFactory().wrap(new byte[0]);
}
static ByteBuf asByteBuf(DataBuffer buffer) {
return buffer instanceof NettyDataBuffer
? ((NettyDataBuffer) buffer).getNativeBuffer()
: Unpooled.wrappedBuffer(buffer.asByteBuffer());
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.gateway.rsocket.test;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@@ -40,9 +41,9 @@ import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.gateway.rsocket.core.GatewayExchange;
import org.springframework.cloud.gateway.rsocket.core.GatewayFilter;
@@ -50,11 +51,17 @@ import org.springframework.cloud.gateway.rsocket.core.GatewayFilterChain;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorExchange;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilter;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilterChain;
import org.springframework.cloud.gateway.rsocket.support.Forwarding;
import org.springframework.cloud.gateway.rsocket.support.Metadata;
import org.springframework.cloud.gateway.rsocket.support.RouteSetup;
import org.springframework.cloud.gateway.rsocket.support.TagsMetadata;
import org.springframework.cloud.gateway.rsocket.support.WellKnownKey;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.messaging.rsocket.RSocketStrategies;
import static io.netty.buffer.Unpooled.EMPTY_BUFFER;
@@ -63,13 +70,13 @@ public class PingPongApp {
@Bean
public Ping ping1() {
return new Ping("1");
return new Ping(1L);
}
@Bean
@ConditionalOnProperty("ping.two.enabled")
public Ping ping2() {
return new Ping("2");
return new Ping(2L);
}
@Bean
@@ -84,11 +91,7 @@ public class PingPongApp {
public static void main(String[] args) {
Hooks.onOperatorDebug();
new SpringApplicationBuilder(PingPongApp.class)
// TODO: remove after
// https://github.com/spring-cloud/spring-cloud-gateway/issues/1140
.properties("spring.main.allow-bean-definition-overriding=true")
.run(args);
SpringApplication.run(PingPongApp.class, args);
}
static String reply(String in) {
@@ -105,6 +108,28 @@ public class PingPongApp {
}
}
static ByteBuf getRouteSetupMetadata(RSocketStrategies strategies, String name,
long id) {
LinkedHashMap<TagsMetadata.Key, String> tags = new LinkedHashMap<>();
tags.put(new TagsMetadata.Key(WellKnownKey.TIME_ZONE),
System.currentTimeMillis() + "");
DataBuffer routeSetup = new MetadataEncoder(Metadata.COMPOSITE_MIME_TYPE,
strategies)
.metadata(new RouteSetup(id, name, tags),
RouteSetup.ROUTE_SETUP_MIME_TYPE)
.encode();
return TagsMetadata.asByteBuf(routeSetup);
}
static ByteBuf getForwardingMetadata(RSocketStrategies strategies, String name,
long id) {
Forwarding metadata = new Forwarding(id, TagsMetadata.builder()
.with(WellKnownKey.SERVICE_NAME, name).build().getTags());
DataBuffer routeSetup = new MetadataEncoder(Metadata.COMPOSITE_MIME_TYPE,
strategies).metadata(metadata, Forwarding.FORWARDING_MIME_TYPE).encode();
return TagsMetadata.asByteBuf(routeSetup);
}
@Slf4j
public static class Ping
implements Ordered, ApplicationListener<ApplicationReadyEvent> {
@@ -112,13 +137,16 @@ public class PingPongApp {
@Autowired
private MeterRegistry meterRegistry;
private final String id;
@Autowired
private RSocketStrategies strategies;
private final Long id;
private final AtomicInteger pongsReceived = new AtomicInteger();
private Flux<String> pongFlux;
public Ping(String id) {
public Ping(Long id) {
this.id = id;
}
@@ -139,17 +167,26 @@ public class PingPongApp {
MicrometerRSocketInterceptor interceptor = new MicrometerRSocketInterceptor(
meterRegistry, Tag.of("component", "ping"));
ByteBuf announcementMetadata = Metadata.from("ping").with("id", "ping" + id)
.encode();
pongFlux = RSocketFactory.connect().frameDecoder(PayloadDecoder.ZERO_COPY)
.metadataMimeType(Metadata.ROUTING_MIME_TYPE)
.setupPayload(
DefaultPayload.create(EMPTY_BUFFER, announcementMetadata))
.addRequesterPlugin(interceptor)
.transport(TcpClientTransport.create(gatewayPort)) // proxy
.start().flatMapMany(socket -> doPing(take, socket));
ByteBuf metadata = getRouteSetupMetadata(strategies, "ping", id);
Payload setupPayload = DefaultPayload.create(EMPTY_BUFFER, metadata);
pongFlux.subscribe();
pongFlux = RSocketFactory.connect().frameDecoder(PayloadDecoder.ZERO_COPY)
.metadataMimeType(Metadata.COMPOSITE_MIME_TYPE.toString())
.setupPayload(setupPayload).addRequesterPlugin(interceptor)
.transport(TcpClientTransport.create(gatewayPort)) // proxy
.start().log("startPing" + id)
.flatMapMany(socket -> doPing(take, socket)).cast(String.class)
.doOnSubscribe(o -> {
if (log.isDebugEnabled()) {
log.debug("ping doOnSubscribe");
}
});
boolean subscribe = env.getProperty("ping.subscribe", Boolean.class, true);
if (subscribe) {
pongFlux.subscribe();
}
}
Publisher<? extends String> doPing(Integer take, RSocket socket) {
@@ -157,7 +194,8 @@ public class PingPongApp {
.requestChannel(Flux.interval(Duration.ofSeconds(1)).map(i -> {
ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT,
"ping" + id);
ByteBuf routingMetadata = Metadata.from("pong").encode();
ByteBuf routingMetadata = getForwardingMetadata(strategies,
"pong", id);
log.debug("Sending ping" + id);
return DefaultPayload.create(data, routingMetadata);
// onBackpressue is needed in case pong is not available yet
@@ -191,6 +229,9 @@ public class PingPongApp {
@Autowired
private MeterRegistry meterRegistry;
@Autowired
private RSocketStrategies strategies;
private final AtomicInteger pingsReceived = new AtomicInteger();
@Override
@@ -213,9 +254,10 @@ public class PingPongApp {
Integer.class, 7002);
MicrometerRSocketInterceptor interceptor = new MicrometerRSocketInterceptor(
meterRegistry, Tag.of("component", "pong"));
ByteBuf announcementMetadata = Metadata.from("pong").with("id", "pong1")
.encode();
RSocketFactory.connect().metadataMimeType(Metadata.ROUTING_MIME_TYPE)
ByteBuf announcementMetadata = getRouteSetupMetadata(strategies, "pong", 3L);
RSocketFactory.connect()
.metadataMimeType(Metadata.COMPOSITE_MIME_TYPE.toString())
.setupPayload(
DefaultPayload.create(EMPTY_BUFFER, announcementMetadata))
.addRequesterPlugin(interceptor).acceptor(this::accept)
@@ -235,7 +277,8 @@ public class PingPongApp {
}).map(PingPongApp::reply).map(reply -> {
ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT,
reply);
ByteBuf routingMetadata = Metadata.from("ping").encode();
ByteBuf routingMetadata = getForwardingMetadata(strategies,
"ping", 1L);
return DefaultPayload.create(data, routingMetadata);
});
}

View File

@@ -22,8 +22,8 @@ import java.util.List;
import org.junit.Test;
import org.springframework.cloud.gateway.rsocket.registry.Registry;
import org.springframework.cloud.gateway.rsocket.registry.RegistrySocketAcceptorFilter;
import org.springframework.cloud.gateway.rsocket.registry.RoutingTable;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilter;
import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorPredicateFilter;
import org.springframework.core.OrderComparator;
@@ -38,7 +38,7 @@ public class SocketAcceptorFilterOrderTests {
SocketAcceptorFilter predicateFilter = new SocketAcceptorPredicateFilter(
Collections.emptyList());
SocketAcceptorFilter registryFilter = new RegistrySocketAcceptorFilter(
mock(Registry.class));
mock(RoutingTable.class));
List<SocketAcceptorFilter> filters = Arrays.asList(predicateFilter,
registryFilter);
OrderComparator.sort(filters);

View File

@@ -1,6 +1,7 @@
logging:
level:
org.springframework.cloud.gateway.rsocket: DEBUG
# org.springframework.cloud.gateway.rsocket: TRACE
management:
endpoints:

View File

@@ -27,4 +27,5 @@
<suppress files=".*XForwardedRemoteAddressResolver\.java" checks="FinalClass"/>
<suppress files=".*Route\.java" checks="FinalClass"/>
<suppress files=".*HystrixGatewayFilterFactory\.java" checks="AvoidNestedBlocks"/>
<suppress files=".*WellKnownKey\.java" checks="JavadocVariable"/>
</suppressions>