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 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 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 metadata = new LinkedHashMap<>(4); + + public MetadataEncoder(MimeType metadataMimeType, RSocketStrategies strategies) { + Assert.notNull(metadataMimeType, "'metadataMimeType' is required"); + Assert.notNull(strategies, "RSocketStrategies is required"); + this.metadataMimeType = metadataMimeType; + this.strategies = strategies; + this.isComposite = this.metadataMimeType.toString() + .equals(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString()); + this.allocator = bufferFactory() instanceof NettyDataBufferFactory + ? ((NettyDataBufferFactory) bufferFactory()).getByteBufAllocator() + : ByteBufAllocator.DEFAULT; + } + + private DataBufferFactory bufferFactory() { + return this.strategies.dataBufferFactory(); + } + + /** + * Set the route to a remote handler as described in + * {@link RSocketRequester#route(String, Object...)}. + */ + public MetadataEncoder route(String route, Object... routeVars) { + this.route = expand(route, routeVars); + assertMetadataEntryCount(); + return this; + } + + private static String expand(String route, Object... routeVars) { + if (ObjectUtils.isEmpty(routeVars)) { + return route; + } + StringBuffer sb = new StringBuffer(); + int index = 0; + Matcher matcher = VARS_PATTERN.matcher(route); + while (matcher.find()) { + Assert.isTrue(index < routeVars.length, + () -> "No value for variable '" + matcher.group(1) + "'"); + String value = routeVars[index].toString(); + value = value.contains(".") ? value.replaceAll("\\.", "%2E") : value; + matcher.appendReplacement(sb, value); + index++; + } + return sb.toString(); + } + + private void assertMetadataEntryCount() { + if (!this.isComposite) { + int count = this.route != null ? this.metadata.size() + 1 + : this.metadata.size(); + Assert.isTrue(count < 2, + "Composite metadata required for multiple metadata entries."); + } + } + + /** + * Add a metadata entry. If called more than once or in addition to route, composite + * metadata must be in use. + */ + public MetadataEncoder metadata(Object metadata, @Nullable MimeType mimeType) { + if (this.isComposite) { + Assert.notNull(mimeType, + "MimeType is required for composite metadata entries."); + } + else if (mimeType == null) { + mimeType = this.metadataMimeType; + } + else if (!this.metadataMimeType.equals(mimeType)) { + throw new IllegalArgumentException("Mime type is optional (may be null) " + + "but was provided and does not match the connection metadata mime type."); + } + this.metadata.put(metadata, mimeType); + assertMetadataEntryCount(); + return this; + } + + /** + * Add route and/or metadata, both optional. + */ + public MetadataEncoder metadataAndOrRoute(@Nullable Map metadata, + @Nullable String route, @Nullable Object[] vars) { + + if (route != null) { + this.route = expand(route, vars != null ? vars : new Object[0]); + } + if (!CollectionUtils.isEmpty(metadata)) { + for (Map.Entry entry : metadata.entrySet()) { + metadata(entry.getKey(), entry.getValue()); + } + } + assertMetadataEntryCount(); + return this; + } + + /** + * Encode the collected metadata entries to a {@code DataBuffer}. + * @see PayloadUtils#createPayload(DataBuffer, DataBuffer) + */ + public DataBuffer encode() { + if (this.isComposite) { + CompositeByteBuf composite = this.allocator.compositeBuffer(); + try { + if (this.route != null) { + CompositeMetadataFlyweight.encodeAndAddMetadata(composite, + this.allocator, WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, + encodeRoute()); + } + this.metadata.forEach((value, mimeType) -> { + ByteBuf metadata = (value instanceof ByteBuf ? (ByteBuf) value + : asByteBuf(encodeEntry(value, mimeType))); + CompositeMetadataFlyweight.encodeAndAddMetadata(composite, + this.allocator, mimeType.toString(), metadata); + }); + return asDataBuffer(composite); + } + catch (Throwable ex) { + composite.release(); + throw ex; + } + } + else if (this.route != null) { + Assert.isTrue(this.metadata.isEmpty(), + "Composite metadata required for route and other entries"); + String routingMimeType = WellKnownMimeType.MESSAGE_RSOCKET_ROUTING + .getString(); + return this.metadataMimeType.toString().equals(routingMimeType) + ? asDataBuffer(encodeRoute()) + : encodeEntry(this.route, this.metadataMimeType); + } + else { + Assert.isTrue(this.metadata.size() == 1, + "Composite metadata required for multiple entries"); + Map.Entry entry = this.metadata.entrySet().iterator() + .next(); + if (!this.metadataMimeType.equals(entry.getValue())) { + throw new IllegalArgumentException( + "Connection configured for metadata mime type " + "'" + + this.metadataMimeType + "', but actual is `" + + this.metadata + "`"); + } + return encodeEntry(entry.getKey(), entry.getValue()); + } + } + + private ByteBuf encodeRoute() { + return TaggingMetadataFlyweight.createRoutingMetadata(this.allocator, + Collections.singletonList(this.route)).getContent(); + } + + @SuppressWarnings("unchecked") + private DataBuffer encodeEntry(Object metadata, MimeType mimeType) { + if (metadata instanceof ByteBuf) { + return asDataBuffer((ByteBuf) metadata); + } + ResolvableType type = ResolvableType.forInstance(metadata); + Encoder encoder = this.strategies.encoder(type, mimeType); + Assert.notNull(encoder, () -> "No encoder for metadata " + metadata + + ", mimeType '" + mimeType + "'"); + return encoder.encodeValue((T) metadata, bufferFactory(), type, mimeType, + Collections.emptyMap()); + } + + private DataBuffer asDataBuffer(ByteBuf byteBuf) { + if (bufferFactory() instanceof NettyDataBufferFactory) { + return ((NettyDataBufferFactory) bufferFactory()).wrap(byteBuf); + } + else { + DataBuffer buffer = bufferFactory().wrap(byteBuf.nioBuffer()); + byteBuf.release(); + return buffer; + } + } + + public static DataBuffer emptyDataBuffer(RSocketStrategies rSocketStrategies) { + return rSocketStrategies.dataBufferFactory().wrap(new byte[0]); + } + + static ByteBuf asByteBuf(DataBuffer buffer) { + return buffer instanceof NettyDataBuffer + ? ((NettyDataBuffer) buffer).getNativeBuffer() + : Unpooled.wrappedBuffer(buffer.asByteBuffer()); + } + +} diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/PingPongApp.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/PingPongApp.java index 757c4e43..05d88ccc 100644 --- a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/PingPongApp.java +++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/PingPongApp.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.rsocket.test; import java.time.Duration; +import java.util.LinkedHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -40,9 +41,9 @@ import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.cloud.gateway.rsocket.core.GatewayExchange; import org.springframework.cloud.gateway.rsocket.core.GatewayFilter; @@ -50,11 +51,17 @@ import org.springframework.cloud.gateway.rsocket.core.GatewayFilterChain; import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorExchange; import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilter; import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilterChain; +import org.springframework.cloud.gateway.rsocket.support.Forwarding; import org.springframework.cloud.gateway.rsocket.support.Metadata; +import org.springframework.cloud.gateway.rsocket.support.RouteSetup; +import org.springframework.cloud.gateway.rsocket.support.TagsMetadata; +import org.springframework.cloud.gateway.rsocket.support.WellKnownKey; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.messaging.rsocket.RSocketStrategies; import static io.netty.buffer.Unpooled.EMPTY_BUFFER; @@ -63,13 +70,13 @@ public class PingPongApp { @Bean public Ping ping1() { - return new Ping("1"); + return new Ping(1L); } @Bean @ConditionalOnProperty("ping.two.enabled") public Ping ping2() { - return new Ping("2"); + return new Ping(2L); } @Bean @@ -84,11 +91,7 @@ public class PingPongApp { public static void main(String[] args) { Hooks.onOperatorDebug(); - new SpringApplicationBuilder(PingPongApp.class) - // TODO: remove after - // https://github.com/spring-cloud/spring-cloud-gateway/issues/1140 - .properties("spring.main.allow-bean-definition-overriding=true") - .run(args); + SpringApplication.run(PingPongApp.class, args); } static String reply(String in) { @@ -105,6 +108,28 @@ public class PingPongApp { } } + static ByteBuf getRouteSetupMetadata(RSocketStrategies strategies, String name, + long id) { + LinkedHashMap tags = new LinkedHashMap<>(); + tags.put(new TagsMetadata.Key(WellKnownKey.TIME_ZONE), + System.currentTimeMillis() + ""); + DataBuffer routeSetup = new MetadataEncoder(Metadata.COMPOSITE_MIME_TYPE, + strategies) + .metadata(new RouteSetup(id, name, tags), + RouteSetup.ROUTE_SETUP_MIME_TYPE) + .encode(); + return TagsMetadata.asByteBuf(routeSetup); + } + + static ByteBuf getForwardingMetadata(RSocketStrategies strategies, String name, + long id) { + Forwarding metadata = new Forwarding(id, TagsMetadata.builder() + .with(WellKnownKey.SERVICE_NAME, name).build().getTags()); + DataBuffer routeSetup = new MetadataEncoder(Metadata.COMPOSITE_MIME_TYPE, + strategies).metadata(metadata, Forwarding.FORWARDING_MIME_TYPE).encode(); + return TagsMetadata.asByteBuf(routeSetup); + } + @Slf4j public static class Ping implements Ordered, ApplicationListener { @@ -112,13 +137,16 @@ public class PingPongApp { @Autowired private MeterRegistry meterRegistry; - private final String id; + @Autowired + private RSocketStrategies strategies; + + private final Long id; private final AtomicInteger pongsReceived = new AtomicInteger(); private Flux pongFlux; - public Ping(String id) { + public Ping(Long id) { this.id = id; } @@ -139,17 +167,26 @@ public class PingPongApp { MicrometerRSocketInterceptor interceptor = new MicrometerRSocketInterceptor( meterRegistry, Tag.of("component", "ping")); - ByteBuf announcementMetadata = Metadata.from("ping").with("id", "ping" + id) - .encode(); - pongFlux = RSocketFactory.connect().frameDecoder(PayloadDecoder.ZERO_COPY) - .metadataMimeType(Metadata.ROUTING_MIME_TYPE) - .setupPayload( - DefaultPayload.create(EMPTY_BUFFER, announcementMetadata)) - .addRequesterPlugin(interceptor) - .transport(TcpClientTransport.create(gatewayPort)) // proxy - .start().flatMapMany(socket -> doPing(take, socket)); + ByteBuf metadata = getRouteSetupMetadata(strategies, "ping", id); + Payload setupPayload = DefaultPayload.create(EMPTY_BUFFER, metadata); - pongFlux.subscribe(); + pongFlux = RSocketFactory.connect().frameDecoder(PayloadDecoder.ZERO_COPY) + .metadataMimeType(Metadata.COMPOSITE_MIME_TYPE.toString()) + .setupPayload(setupPayload).addRequesterPlugin(interceptor) + .transport(TcpClientTransport.create(gatewayPort)) // proxy + .start().log("startPing" + id) + .flatMapMany(socket -> doPing(take, socket)).cast(String.class) + .doOnSubscribe(o -> { + if (log.isDebugEnabled()) { + log.debug("ping doOnSubscribe"); + } + }); + + boolean subscribe = env.getProperty("ping.subscribe", Boolean.class, true); + + if (subscribe) { + pongFlux.subscribe(); + } } Publisher doPing(Integer take, RSocket socket) { @@ -157,7 +194,8 @@ public class PingPongApp { .requestChannel(Flux.interval(Duration.ofSeconds(1)).map(i -> { ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "ping" + id); - ByteBuf routingMetadata = Metadata.from("pong").encode(); + ByteBuf routingMetadata = getForwardingMetadata(strategies, + "pong", id); log.debug("Sending ping" + id); return DefaultPayload.create(data, routingMetadata); // onBackpressue is needed in case pong is not available yet @@ -191,6 +229,9 @@ public class PingPongApp { @Autowired private MeterRegistry meterRegistry; + @Autowired + private RSocketStrategies strategies; + private final AtomicInteger pingsReceived = new AtomicInteger(); @Override @@ -213,9 +254,10 @@ public class PingPongApp { Integer.class, 7002); MicrometerRSocketInterceptor interceptor = new MicrometerRSocketInterceptor( meterRegistry, Tag.of("component", "pong")); - ByteBuf announcementMetadata = Metadata.from("pong").with("id", "pong1") - .encode(); - RSocketFactory.connect().metadataMimeType(Metadata.ROUTING_MIME_TYPE) + + ByteBuf announcementMetadata = getRouteSetupMetadata(strategies, "pong", 3L); + RSocketFactory.connect() + .metadataMimeType(Metadata.COMPOSITE_MIME_TYPE.toString()) .setupPayload( DefaultPayload.create(EMPTY_BUFFER, announcementMetadata)) .addRequesterPlugin(interceptor).acceptor(this::accept) @@ -235,7 +277,8 @@ public class PingPongApp { }).map(PingPongApp::reply).map(reply -> { ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, reply); - ByteBuf routingMetadata = Metadata.from("ping").encode(); + ByteBuf routingMetadata = getForwardingMetadata(strategies, + "ping", 1L); return DefaultPayload.create(data, routingMetadata); }); } diff --git a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/SocketAcceptorFilterOrderTests.java b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/SocketAcceptorFilterOrderTests.java index c3c52921..11f512a5 100644 --- a/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/SocketAcceptorFilterOrderTests.java +++ b/spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/SocketAcceptorFilterOrderTests.java @@ -22,8 +22,8 @@ import java.util.List; import org.junit.Test; -import org.springframework.cloud.gateway.rsocket.registry.Registry; import org.springframework.cloud.gateway.rsocket.registry.RegistrySocketAcceptorFilter; +import org.springframework.cloud.gateway.rsocket.registry.RoutingTable; import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorFilter; import org.springframework.cloud.gateway.rsocket.socketacceptor.SocketAcceptorPredicateFilter; import org.springframework.core.OrderComparator; @@ -38,7 +38,7 @@ public class SocketAcceptorFilterOrderTests { SocketAcceptorFilter predicateFilter = new SocketAcceptorPredicateFilter( Collections.emptyList()); SocketAcceptorFilter registryFilter = new RegistrySocketAcceptorFilter( - mock(Registry.class)); + mock(RoutingTable.class)); List filters = Arrays.asList(predicateFilter, registryFilter); OrderComparator.sort(filters); diff --git a/spring-cloud-gateway-rsocket/src/test/resources/application.yml b/spring-cloud-gateway-rsocket/src/test/resources/application.yml index e12210c9..ba035fdf 100644 --- a/spring-cloud-gateway-rsocket/src/test/resources/application.yml +++ b/spring-cloud-gateway-rsocket/src/test/resources/application.yml @@ -1,6 +1,7 @@ logging: level: org.springframework.cloud.gateway.rsocket: DEBUG +# org.springframework.cloud.gateway.rsocket: TRACE management: endpoints: diff --git a/src/checkstyle/checkstyle-suppressions.xml b/src/checkstyle/checkstyle-suppressions.xml index 9cfd624e..a50235e9 100644 --- a/src/checkstyle/checkstyle-suppressions.xml +++ b/src/checkstyle/checkstyle-suppressions.xml @@ -27,4 +27,5 @@ + \ No newline at end of file