diff --git a/pom.xml b/pom.xml
index 5ac0cf9b..37670fce 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,7 +52,7 @@
UTF-8
UTF-8
1.8
- 1.0.0-RC2
+ 1.0.0-RC3-SNAPSHOT
2.2.0.BUILD-SNAPSHOT
2.2.0.BUILD-SNAPSHOT
0.6
@@ -212,6 +212,15 @@
false
+
+
+ jfrog-snapshots
+ JFRog Snapshots
+ https://oss.jfrog.org/artifactory/libs-snapshot
+
+ true
+
+
diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml
index cff14aa2..17df4828 100644
--- a/spring-cloud-gateway-dependencies/pom.xml
+++ b/spring-cloud-gateway-dependencies/pom.xml
@@ -18,6 +18,7 @@
Spring Cloud Gateway Dependencies
+ 0.8.9
@@ -47,6 +48,11 @@
spring-cloud-starter-gateway
${project.version}
+
+ org.roaringbitmap
+ RoaringBitmap
+ ${roaringbitmap.version}
+
diff --git a/spring-cloud-gateway-rsocket/pom.xml b/spring-cloud-gateway-rsocket/pom.xml
index b9d11e09..1b9d86e4 100644
--- a/spring-cloud-gateway-rsocket/pom.xml
+++ b/spring-cloud-gateway-rsocket/pom.xml
@@ -56,6 +56,10 @@
io.micrometer
micrometer-core
+
+ org.roaringbitmap
+ RoaringBitmap
+
org.springframework.boot
spring-boot-starter-actuator
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/autoconfigure/GatewayRSocketAutoConfiguration.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/autoconfigure/GatewayRSocketAutoConfiguration.java
index 5e278bbb..d329dc5e 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/autoconfigure/GatewayRSocketAutoConfiguration.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/autoconfigure/GatewayRSocketAutoConfiguration.java
@@ -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 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());
+ };
+ }
+
}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/AbstractGatewayRSocket.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/AbstractGatewayRSocket.java
new file mode 100644
index 00000000..6549ef71
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/AbstractGatewayRSocket.java
@@ -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 doOnEmpty(GatewayExchange exchange) {
+ if (log.isDebugEnabled()) {
+ log.debug("Unable to find destination RSocket for "
+ + exchange.getRoutingMetadata());
+ }
+ return Mono.empty();
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayExchange.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayExchange.java
index c5a9a30f..bdfbd9f6 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayExchange.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayExchange.java
@@ -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 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;
}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocket.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocket.java
index 6aa98137..40f9b983 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocket.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocket.java
@@ -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 requestChannel(Payload payload, Publisher 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 requestResponse(Payload payload) {
AtomicReference timer = new AtomicReference<>();
@@ -211,56 +140,7 @@ public class GatewayRSocket extends AbstractRSocket implements ResponderRSocket
private Mono findRSocketOrCreatePending(GatewayExchange exchange) {
return findRSocket(exchange)
// if a route can't be found or registered RSocket, create pending
- .switchIfEmpty(createPendingRSocket(exchange));
- }
-
- private Mono 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> 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 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 findRoute(GatewayExchange exchange) {
- Mono routeMono;
- /*
- * if (this.route != null) { //TODO: cache Route? routeMono = Mono.just(route); }
- * else {
- */
- routeMono = this.routes.findRoute(exchange);
- // }
- return routeMono;
- }
-
- private Mono 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 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 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));
}
}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketFactory.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketFactory.java
new file mode 100644
index 00000000..c37d6da5
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketFactory.java
@@ -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;
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/PendingRequestRSocket.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/PendingRequestRSocket.java
index a8dda50b..2a33d029 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/PendingRequestRSocket.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/PendingRequestRSocket.java
@@ -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> routeFinder;
- private final Consumer metadataCallback;
+ private final MetadataExtractor metadataExtractor;
+
+ private final Consumer metadataCallback;
private final MonoProcessor rSocketProcessor;
@@ -57,16 +61,18 @@ public class PendingRequestRSocket extends AbstractRSocket
private Route route;
- public PendingRequestRSocket(Function> routeFinder,
- Consumer metadataCallback) {
- this(routeFinder, metadataCallback, MonoProcessor.create());
+ public PendingRequestRSocket(MetadataExtractor metadataExtractor,
+ Function> routeFinder,
+ Consumer metadataCallback) {
+ this(metadataExtractor, routeFinder, metadataCallback, MonoProcessor.create());
}
- /* for testing */ PendingRequestRSocket(
+ /* for testing */ PendingRequestRSocket(MetadataExtractor metadataExtractor,
Function> routeFinder,
- Consumer metadataCallback,
+ Consumer metadataCallback,
MonoProcessor 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)
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/PendingRequestRSocketFactory.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/PendingRequestRSocketFactory.java
new file mode 100644
index 00000000..f5570b6c
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/PendingRequestRSocketFactory.java
@@ -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 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> routeFinder = registeredEvent -> getRouteMono(
+ registeredEvent, exchange);
+ Consumer 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 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 matchRoute(Route route, TagsMetadata tagsMetadata) {
+ Set routeIds = this.routingTable.findRouteIds(tagsMetadata);
+ if (routeIds.contains(route.getId())) {
+ return Mono.just(route);
+ }
+ return Mono.empty();
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/LoadBalancedRSocket.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/LoadBalancedRSocket.java
deleted file mode 100644
index 969706bd..00000000
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/LoadBalancedRSocket.java
+++ /dev/null
@@ -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 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 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 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 as input?
- // TODO: reuse commons load balancer?
- public interface LoadBalancer
- extends Function, Mono> {
-
- }
-
- 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 apply(List 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);
- }
-
- }
-
-}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/LoadBalancerFactory.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/LoadBalancerFactory.java
new file mode 100644
index 00000000..c43288bb
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/LoadBalancerFactory.java
@@ -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> choose(TagsMetadata tagsMetadata) {
+ List> 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 as input?
+ // TODO: reuse commons load balancer?
+ public interface LoadBalancer extends
+ Function>, Mono>> {
+
+ }
+
+ 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> apply(
+ List> 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 tuple = rSockets.get(pos % rSockets.size());
+ return Mono.just(tuple);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/Registry.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/Registry.java
deleted file mode 100644
index db7336f4..00000000
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/Registry.java
+++ /dev/null
@@ -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 rsockets = new ConcurrentHashMap<>();
-
- private final DirectProcessor registeredEvents = DirectProcessor
- .create();
-
- private final FluxSink registeredEventsSink = registeredEvents
- .sink(FluxSink.OverflowStrategy.DROP);
-
- public Registry() {
- }
-
- // TODO: Mono?
- 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 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;
- }
-
- }
-
-}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RegistryRoutes.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RegistryRoutes.java
deleted file mode 100644
index dd596f0d..00000000
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RegistryRoutes.java
+++ /dev/null
@@ -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 {
-
- private static final Log log = LogFactory.getLog(RegistryRoutes.class);
-
- private Map routes = new ConcurrentHashMap<>();
-
- @Override
- public Flux getRoutes() {
- // TODO: sorting
- // TODO: caching
- Collection 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;
- }
-
-}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RegistrySocketAcceptorFilter.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RegistrySocketAcceptorFilter.java
index 4ae15f00..2b711163 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RegistrySocketAcceptorFilter.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RegistrySocketAcceptorFilter.java
@@ -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 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);
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTable.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTable.java
new file mode 100644
index 00000000..58070894
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTable.java
@@ -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 internalRouteIdToRouteId = new ConcurrentHashMap<>();
+
+ final Map tagsToBitmaps = new ConcurrentHashMap<>();
+
+ final Map routeIdToRSocket = new ConcurrentHashMap<>();
+
+ final Map routeIdToTags = new ConcurrentHashMap<>();
+
+ private final DirectProcessor registeredEvents = DirectProcessor
+ .create();
+
+ private final FluxSink registeredEventsSink = registeredEvents
+ .sink(FluxSink.OverflowStrategy.DROP);
+
+ public RoutingTable() {
+ }
+
+ // TODO: Mono?
+ 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 findRouteIds(TagsMetadata tagsMetadata) {
+ RoaringBitmap found = find(tagsMetadata);
+ if (found.isEmpty()) {
+ return Collections.emptySet();
+ }
+ HashSet 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> findRSockets(TagsMetadata tagsMetadata) {
+ RoaringBitmap found = find(tagsMetadata);
+ if (found.isEmpty()) {
+ return Collections.emptyList();
+ }
+ ArrayList> 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 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();
+
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableRoutes.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableRoutes.java
new file mode 100644
index 00000000..401155a7
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableRoutes.java
@@ -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 {
+
+ private static final Log log = LogFactory.getLog(RoutingTableRoutes.class);
+
+ private Map routes = new ConcurrentHashMap<>();
+
+ private final RoutingTable routingTable;
+
+ public RoutingTableRoutes(RoutingTable routingTable) {
+ this.routingTable = routingTable;
+ this.routingTable.addListener(this);
+ }
+
+ @Override
+ public Flux getRoutes() {
+ // TODO: sorting?
+ // TODO: caching
+
+ Collection 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 predicate = exchange -> {
+ // TODO: standard predicates
+ // TODO: allow customized predicates
+ Set 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 predicate;
+
+ RegistryRoute(String id, AsyncPredicate predicate) {
+ this.id = id;
+ this.predicate = predicate;
+ }
+
+ @Override
+ public String getId() {
+ return this.id;
+ }
+
+ @Override
+ public AsyncPredicate getPredicate() {
+ return this.predicate;
+ }
+
+ @Override
+ public List 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();
+
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/route/DefaultRoute.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/route/DefaultRoute.java
new file mode 100644
index 00000000..03583c64
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/route/DefaultRoute.java
@@ -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 predicate;
+
+ private final List gatewayFilters;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ private DefaultRoute(String id, RouteSetup targetMetadata, int order,
+ AsyncPredicate predicate,
+ List 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 getPredicate() {
+ return this.predicate;
+ }
+
+ public List 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 predicate;
+
+ protected List 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 getPredicate() {
+ return this.predicate;
+ }
+
+ public Builder routingMetadata(RouteSetup routingMetadata) {
+ this.routingMetadata = routingMetadata;
+ return this;
+ }
+
+ public Builder setFilters(List gatewayFilters) {
+ this.gatewayFilters = gatewayFilters;
+ return this;
+ }
+
+ public Builder filter(GatewayFilter gatewayFilter) {
+ this.gatewayFilters.add(gatewayFilter);
+ return this;
+ }
+
+ public Builder filters(Collection gatewayFilters) {
+ this.gatewayFilters.addAll(gatewayFilters);
+ return this;
+ }
+
+ public Builder filters(GatewayFilter... gatewayFilters) {
+ return filters(Arrays.asList(gatewayFilters));
+ }
+
+ public Builder predicate(AsyncPredicate predicate) {
+ this.predicate = predicate;
+ return this;
+ }
+
+ public Builder and(AsyncPredicate predicate) {
+ Assert.notNull(this.predicate, "can not call and() on null predicate");
+ this.predicate = this.predicate.and(predicate);
+ return this;
+ }
+
+ public Builder or(AsyncPredicate 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);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/route/Route.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/route/Route.java
index c9a3d29e..404fff23 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/route/Route.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/route/Route.java
@@ -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 predicate;
-
- private final List gatewayFilters;
-
- public static Builder builder() {
- return new Builder();
+ default int getOrder() {
+ return 0;
}
- private Route(String id, Metadata targetMetadata, int order,
- AsyncPredicate predicate,
- List gatewayFilters) {
- this.id = id;
- this.targetMetadata = targetMetadata;
- this.order = order;
- this.predicate = predicate;
- this.gatewayFilters = gatewayFilters;
- }
+ AsyncPredicate getPredicate();
- public String getId() {
- return this.id;
- }
-
- public Metadata getTargetMetadata() {
- return this.targetMetadata;
- }
-
- public int getOrder() {
- return order;
- }
-
- public AsyncPredicate getPredicate() {
- return this.predicate;
- }
-
- public List 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 predicate;
-
- protected List 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 getPredicate() {
- return this.predicate;
- }
-
- public Builder routingMetadata(Metadata routingMetadata) {
- this.routingMetadata = routingMetadata;
- return this;
- }
-
- public Builder setFilters(List gatewayFilters) {
- this.gatewayFilters = gatewayFilters;
- return this;
- }
-
- public Builder filter(GatewayFilter gatewayFilter) {
- this.gatewayFilters.add(gatewayFilter);
- return this;
- }
-
- public Builder filters(Collection gatewayFilters) {
- this.gatewayFilters.addAll(gatewayFilters);
- return this;
- }
-
- public Builder filters(GatewayFilter... gatewayFilters) {
- return filters(Arrays.asList(gatewayFilters));
- }
-
- public Builder predicate(AsyncPredicate predicate) {
- this.predicate = predicate;
- return this;
- }
-
- public Builder and(AsyncPredicate predicate) {
- Assert.notNull(this.predicate, "can not call and() on null predicate");
- this.predicate = this.predicate.and(predicate);
- return this;
- }
-
- public Builder or(AsyncPredicate 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 getFilters();
}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/socketacceptor/GatewaySocketAcceptor.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/socketacceptor/GatewaySocketAcceptor.java
index 0ed9b0aa..ccd6fb4f 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/socketacceptor/GatewaySocketAcceptor.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/socketacceptor/GatewaySocketAcceptor.java
@@ -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 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 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) {
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/socketacceptor/SocketAcceptorExchange.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/socketacceptor/SocketAcceptorExchange.java
index 57a1689d..92fe786f 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/socketacceptor/SocketAcceptorExchange.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/socketacceptor/SocketAcceptorExchange.java
@@ -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;
}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/Forwarding.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/Forwarding.java
new file mode 100644
index 00000000..145804b4
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/Forwarding.java
@@ -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 tags) {
+ this(BigInteger.valueOf(originRouteId), tags);
+ }
+
+ public Forwarding(BigInteger originRouteId, Map 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 {
+
+ public Encoder() {
+ super(Forwarding.FORWARDING_MIME_TYPE);
+ }
+
+ @Override
+ public Flux encode(Publisher extends Forwarding> inputStream,
+ DataBufferFactory bufferFactory, ResolvableType elementType,
+ MimeType mimeType, Map hints) {
+ throw new UnsupportedOperationException("stream encoding not supported.");
+ }
+
+ @Override
+ public DataBuffer encodeValue(Forwarding value, DataBufferFactory bufferFactory,
+ ResolvableType valueType, MimeType mimeType, Map hints) {
+ NettyDataBufferFactory factory = (NettyDataBufferFactory) bufferFactory;
+ ByteBuf encoded = Forwarding.encode(factory.getByteBufAllocator(), value);
+ return factory.wrap(encoded);
+ }
+
+ }
+
+ public static class Decoder extends AbstractDecoder {
+
+ public Decoder() {
+ super(Forwarding.FORWARDING_MIME_TYPE);
+ }
+
+ @Override
+ public Flux decode(Publisher inputStream,
+ ResolvableType elementType, MimeType mimeType,
+ Map hints) {
+ throw new UnsupportedOperationException("stream decoding not supported.");
+ }
+
+ @Override
+ public Forwarding decode(DataBuffer buffer, ResolvableType targetType,
+ MimeType mimeType, Map hints) throws DecodingException {
+ ByteBuf byteBuf = TagsMetadata.asByteBuf(buffer);
+ return Forwarding.decode(byteBuf);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/Metadata.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/Metadata.java
index 4b7ea73c..9f38a5c8 100644
--- a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/Metadata.java
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/Metadata.java
@@ -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 properties;
-
- public Metadata(String name, Map properties) {
- this.name = name;
- this.properties = properties;
- }
-
- public String getName() {
- return this.name;
- }
-
- public Map 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 properties) {
- return encode(ByteBufAllocator.DEFAULT, name, properties);
- }
-
- public static ByteBuf encode(ByteBufAllocator allocator, String name,
- Map 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 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 leftMetadata,
- Map rightMetadata) {
- if (leftMetadata == null || rightMetadata == null) {
- return false;
- }
-
- for (Map.Entry 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());
}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/RouteSetup.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/RouteSetup.java
new file mode 100644
index 00000000..b6a6f22f
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/RouteSetup.java
@@ -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 tags) {
+ this(BigInteger.valueOf(id), serviceName, tags);
+ }
+
+ public RouteSetup(BigInteger id, String serviceName, Map 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 {
+
+ public Encoder() {
+ super(ROUTE_SETUP_MIME_TYPE);
+ }
+
+ @Override
+ public Flux encode(Publisher extends RouteSetup> inputStream,
+ DataBufferFactory bufferFactory, ResolvableType elementType,
+ MimeType mimeType, Map hints) {
+ throw new UnsupportedOperationException("stream encoding not supported.");
+ }
+
+ @Override
+ public DataBuffer encodeValue(RouteSetup value, DataBufferFactory bufferFactory,
+ ResolvableType valueType, MimeType mimeType, Map hints) {
+ NettyDataBufferFactory factory = (NettyDataBufferFactory) bufferFactory;
+ ByteBuf encoded = RouteSetup.encode(factory.getByteBufAllocator(), value);
+ return factory.wrap(encoded);
+ }
+
+ }
+
+ public static class Decoder extends AbstractDecoder {
+
+ public Decoder() {
+ super(ROUTE_SETUP_MIME_TYPE);
+ }
+
+ @Override
+ public Flux decode(Publisher inputStream,
+ ResolvableType elementType, MimeType mimeType,
+ Map hints) {
+ throw new UnsupportedOperationException("stream decoding not supported.");
+ }
+
+ @Override
+ public RouteSetup decode(DataBuffer buffer, ResolvableType targetType,
+ MimeType mimeType, Map hints) throws DecodingException {
+ ByteBuf byteBuf = TagsMetadata.asByteBuf(buffer);
+ return RouteSetup.decode(byteBuf);
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/TagsMetadata.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/TagsMetadata.java
new file mode 100644
index 00000000..83c6e0a9
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/TagsMetadata.java
@@ -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 tags;
+
+ TagsMetadata(Map tags) {
+ this.tags = tags;
+ }
+
+ public static ByteBuf asByteBuf(DataBuffer buffer) {
+ return buffer instanceof NettyDataBuffer
+ ? ((NettyDataBuffer) buffer).getNativeBuffer()
+ : Unpooled.wrappedBuffer(buffer.asByteBuffer());
+ }
+
+ public Map 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 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 tags) {
+ Assert.notNull(byteBuf, "byteBuf may not be null");
+
+ Iterator> it = tags.entrySet().iterator();
+
+ while (it.hasNext()) {
+ Map.Entry 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();
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/WellKnownKey.java b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/WellKnownKey.java
new file mode 100644
index 00000000..3220a9e0
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/WellKnownKey.java
@@ -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 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;
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/autoconfigure/GatewayRSocketAutoConfigurationTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/autoconfigure/GatewayRSocketAutoConfigurationTests.java
index 55040727..4c88d735 100644
--- a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/autoconfigure/GatewayRSocketAutoConfigurationTests.java
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/autoconfigure/GatewayRSocketAutoConfigurationTests.java
@@ -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)
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketIntegrationTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketIntegrationTests.java
index 901bfb58..9c959c7b 100644
--- a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketIntegrationTests.java
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketIntegrationTests.java
@@ -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));
}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketTests.java
index c7c0fee5..90c78864 100644
--- a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketTests.java
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketTests.java
@@ -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 mono = Mono
- .just(new EnrichedRSocket(rSocket, getMetadata()));
- when(loadBalancedRSocket.choose()).thenReturn(mono);
+ Tuple2 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 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 getProcessor() {
+ TestPendingFactory factory = (TestPendingFactory) super.getPendingFactory();
+ return factory.processor;
+ }
+
+ }
+
+ private static class TestPendingFactory extends PendingRequestRSocketFactory {
+
private final MonoProcessor 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> routeFinder = registeredEvent -> getRouteMono(
+ protected PendingRequestRSocket constructPendingRSocket(
+ GatewayExchange exchange) {
+ Function> 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 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 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();
}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableRoutesTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableRoutesTests.java
new file mode 100644
index 00000000..e8fb1fc3
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableRoutesTests.java
@@ -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 routeIds = new HashSet<>();
+ routeIds.add("2");
+ when(routingTable.findRouteIds(any(TagsMetadata.class))).thenReturn(routeIds);
+ addRoute(routes, "1");
+ addRoute(routes, "2");
+ addRoute(routes, "3");
+
+ HashMap tags = new HashMap<>();
+ tags.put(new TagsMetadata.Key(WellKnownKey.ROUTE_ID), "2");
+ Forwarding forwarding = new Forwarding(1L, tags);
+ Mono 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)));
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableTests.java
new file mode 100644
index 00000000..bfe8e6c4
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableTests.java
@@ -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> 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> 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> 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();
+
+ }
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/socketacceptor/GatewaySocketAcceptorTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/socketacceptor/GatewaySocketAcceptorTests.java
index 2bbd9ff5..3196309b 100644
--- a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/socketacceptor/GatewaySocketAcceptorTests.java
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/socketacceptor/GatewaySocketAcceptorTests.java
@@ -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));
}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/ForwardingIntegrationTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/ForwardingIntegrationTests.java
new file mode 100644
index 00000000..6dc81149
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/ForwardingIntegrationTests.java
@@ -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 metadata = metadataExtractor.extract(payload,
+ Metadata.COMPOSITE_MIME_TYPE);
+ assertThat(metadata).containsKey("forwarding");
+
+ return (Forwarding) metadata.get("forwarding");
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class Config {
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/ForwardingTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/ForwardingTests.java
new file mode 100644
index 00000000..77f5bb67
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/ForwardingTests.java
@@ -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 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);
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/MetadataTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/MetadataTests.java
deleted file mode 100644
index b5d56ea0..00000000
--- a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/MetadataTests.java
+++ /dev/null
@@ -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 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 metadata(int size) {
- Assert.isTrue(size > 0, "size must be > 0");
- HashMap metadata = new HashMap<>();
- IntStream.rangeClosed(1, size).forEach(i -> metadata.put("key" + i, "val" + i));
- return metadata;
- }
-
-}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/RouteSetupIntegrationTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/RouteSetupIntegrationTests.java
new file mode 100644
index 00000000..830ba858
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/RouteSetupIntegrationTests.java
@@ -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 metadata = metadataExtractor.extract(payload,
+ Metadata.COMPOSITE_MIME_TYPE);
+ assertThat(metadata).containsKey("routesetup");
+
+ return (RouteSetup) metadata.get("routesetup");
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration
+ static class Config {
+
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/RouteSetupTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/RouteSetupTests.java
new file mode 100644
index 00000000..9a1cd204
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/RouteSetupTests.java
@@ -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 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);
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/TagsMetadataTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/TagsMetadataTests.java
new file mode 100644
index 00000000..8557fd2a
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/TagsMetadataTests.java
@@ -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");
+ }
+
+}
diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/MetadataEncoder.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/MetadataEncoder.java
new file mode 100644
index 00000000..b87632e9
--- /dev/null
+++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/MetadataEncoder.java
@@ -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