From 1c4a8518bcdb9165eb31beb551db254f6d26e67e Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Tue, 13 Aug 2019 16:29:25 -0500 Subject: [PATCH] Adds support for RSocket composite metadata. Renames Registry to RoutingTable implemented as an inverted index as TagsMetadata. Inverted index uses RoaringBitmaps to save memory and to make lookup very fast. Implements RoutingTableRoutes as a view over RoutingTable. Route is now an interface. The Route objects returned by RoutingTableRoutes are backed by a predicate that uses the RoutingTable rather than custom matching. GatewayRSockectFactory moved to a top level class. PendingRequestRSocket also has a top level Factory. Load balancing is now handled by a Factory. Allows requests to ask for specific types of load balancers in the future. Implements proposed RouteSetup and Forwarding metadata types in the Gateway. These should eventually be replaced by primitives in rsocket-java. Also adds support to be encoded/decoded using Frameworks Encoder/Decoder classes. fixes gh-1236 --- pom.xml | 11 +- spring-cloud-gateway-dependencies/pom.xml | 6 + spring-cloud-gateway-rsocket/pom.xml | 4 + .../GatewayRSocketAutoConfiguration.java | 84 ++++- .../rsocket/core/AbstractGatewayRSocket.java | 117 ++++++ .../gateway/rsocket/core/GatewayExchange.java | 58 ++- .../gateway/rsocket/core/GatewayRSocket.java | 206 ++--------- .../rsocket/core/GatewayRSocketFactory.java | 90 +++++ .../rsocket/core/PendingRequestRSocket.java | 24 +- .../core/PendingRequestRSocketFactory.java | 105 ++++++ .../rsocket/registry/LoadBalancedRSocket.java | 129 ------- .../rsocket/registry/LoadBalancerFactory.java | 91 +++++ .../gateway/rsocket/registry/Registry.java | 110 ------ .../rsocket/registry/RegistryRoutes.java | 90 ----- .../RegistrySocketAcceptorFilter.java | 15 +- .../rsocket/registry/RoutingTable.java | 287 +++++++++++++++ .../rsocket/registry/RoutingTableRoutes.java | 149 ++++++++ .../gateway/rsocket/route/DefaultRoute.java | 197 ++++++++++ .../cloud/gateway/rsocket/route/Route.java | 174 +-------- .../socketacceptor/GatewaySocketAcceptor.java | 50 ++- .../SocketAcceptorExchange.java | 10 +- .../gateway/rsocket/support/Forwarding.java | 155 ++++++++ .../gateway/rsocket/support/Metadata.java | 188 +--------- .../gateway/rsocket/support/RouteSetup.java | 178 +++++++++ .../gateway/rsocket/support/TagsMetadata.java | 345 ++++++++++++++++++ .../gateway/rsocket/support/WellKnownKey.java | 131 +++++++ .../GatewayRSocketAutoConfigurationTests.java | 12 +- .../core/GatewayRSocketIntegrationTests.java | 5 +- .../rsocket/core/GatewayRSocketTests.java | 131 ++++--- .../registry/RoutingTableRoutesTests.java | 75 ++++ .../rsocket/registry/RoutingTableTests.java | 170 +++++++++ .../GatewaySocketAcceptorTests.java | 55 ++- .../support/ForwardingIntegrationTests.java | 72 ++++ .../rsocket/support/ForwardingTests.java | 75 ++++ .../rsocket/support/MetadataTests.java | 85 ----- .../support/RouteSetupIntegrationTests.java | 72 ++++ .../rsocket/support/RouteSetupTests.java | 113 ++++++ .../rsocket/support/TagsMetadataTests.java | 63 ++++ .../gateway/rsocket/test/MetadataEncoder.java | 256 +++++++++++++ .../gateway/rsocket/test/PingPongApp.java | 93 +++-- .../test/SocketAcceptorFilterOrderTests.java | 4 +- .../src/test/resources/application.yml | 1 + src/checkstyle/checkstyle-suppressions.xml | 1 + 43 files changed, 3188 insertions(+), 1099 deletions(-) create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/AbstractGatewayRSocket.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/GatewayRSocketFactory.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/core/PendingRequestRSocketFactory.java delete mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/LoadBalancedRSocket.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/LoadBalancerFactory.java delete mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/Registry.java delete mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RegistryRoutes.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTable.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableRoutes.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/route/DefaultRoute.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/Forwarding.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/RouteSetup.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/TagsMetadata.java create mode 100644 spring-cloud-gateway-rsocket/src/main/java/org/springframework/cloud/gateway/rsocket/support/WellKnownKey.java create mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableRoutesTests.java create mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/registry/RoutingTableTests.java create mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/ForwardingIntegrationTests.java create mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/ForwardingTests.java delete mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/MetadataTests.java create mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/RouteSetupIntegrationTests.java create mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/RouteSetupTests.java create mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/support/TagsMetadataTests.java create mode 100644 spring-cloud-gateway-rsocket/src/test/java/org/springframework/cloud/gateway/rsocket/test/MetadataEncoder.java 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