diff --git a/services-api/src/main/java/io/scalecube/services/ServiceCall.java b/services-api/src/main/java/io/scalecube/services/ServiceCall.java index f95875bf6..90930cf10 100644 --- a/services-api/src/main/java/io/scalecube/services/ServiceCall.java +++ b/services-api/src/main/java/io/scalecube/services/ServiceCall.java @@ -26,7 +26,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public class ServiceCall { +public class ServiceCall implements AutoCloseable { private ClientTransport transport; private ServiceRegistry serviceRegistry; @@ -400,4 +400,15 @@ private ServiceMessage throwIfError(ServiceMessage message) { } return message; } + + @Override + public void close() { + if (transport != null) { + try { + transport.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } } diff --git a/services-api/src/main/java/io/scalecube/services/transport/api/ClientTransport.java b/services-api/src/main/java/io/scalecube/services/transport/api/ClientTransport.java index 8c0a7aa12..5ef1d1bda 100644 --- a/services-api/src/main/java/io/scalecube/services/transport/api/ClientTransport.java +++ b/services-api/src/main/java/io/scalecube/services/transport/api/ClientTransport.java @@ -2,10 +2,10 @@ import io.scalecube.services.ServiceReference; -public interface ClientTransport { +public interface ClientTransport extends AutoCloseable { /** - * Creates {@link ClientChannel} ready for communication with remote service endpoint. + * Creates {@link ClientChannel} for communication with remote service endpoint. * * @param serviceReference target serviceReference * @return {@code ClientChannel} instance diff --git a/services-gateway/pom.xml b/services-gateway/pom.xml index 74892d29c..8bfff3973 100644 --- a/services-gateway/pom.xml +++ b/services-gateway/pom.xml @@ -1,5 +1,7 @@ - + 4.0.0 @@ -14,7 +16,7 @@ io.scalecube - scalecube-services + scalecube-services-api ${project.parent.version} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/GatewayTemplate.java b/services-gateway/src/main/java/io/scalecube/services/gateway/GatewayTemplate.java deleted file mode 100644 index dbc5af217..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/GatewayTemplate.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.scalecube.services.gateway; - -import java.net.InetSocketAddress; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; -import reactor.netty.DisposableServer; -import reactor.netty.http.server.HttpServer; -import reactor.netty.resources.LoopResources; - -public abstract class GatewayTemplate implements Gateway { - - private static final Logger LOGGER = LoggerFactory.getLogger(GatewayTemplate.class); - - protected final GatewayOptions options; - - protected GatewayTemplate(GatewayOptions options) { - this.options = - new GatewayOptions() - .id(options.id()) - .port(options.port()) - .workerPool(options.workerPool()) - .call(options.call()); - } - - @Override - public final String id() { - return options.id(); - } - - /** - * Builds generic http server with given parameters. - * - * @param loopResources loop resources - * @param port listen port - * @return http server - */ - protected HttpServer prepareHttpServer(LoopResources loopResources, int port) { - return HttpServer.create() - .tcpConfiguration( - tcpServer -> { - if (loopResources != null) { - tcpServer = tcpServer.runOn(loopResources); - } - return tcpServer.bindAddress(() -> new InetSocketAddress(port)); - }); - } - - /** - * Shutting down loopResources if it's not null. - * - * @return mono handle - */ - protected final Mono shutdownLoopResources(LoopResources loopResources) { - return Mono.defer( - () -> { - if (loopResources == null) { - return Mono.empty(); - } - return loopResources - .disposeLater() - .doOnError(e -> LOGGER.warn("Failed to close loopResources: " + e)); - }); - } - - /** - * Shutting down server of type {@link DisposableServer} if it's not null. - * - * @param server server - * @return mono hanle - */ - protected final Mono shutdownServer(DisposableServer server) { - return Mono.defer( - () -> { - if (server == null) { - return Mono.empty(); - } - server.dispose(); - return server.onDispose().doOnError(e -> LOGGER.warn("Failed to close server: " + e)); - }); - } -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/client/GatewayClientCodec.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/GatewayClientCodec.java new file mode 100644 index 000000000..46c92ba87 --- /dev/null +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/GatewayClientCodec.java @@ -0,0 +1,38 @@ +package io.scalecube.services.gateway.client; + +import io.netty.buffer.ByteBuf; +import io.scalecube.services.api.ServiceMessage; +import io.scalecube.services.exceptions.MessageCodecException; +import java.lang.reflect.Type; + +public interface GatewayClientCodec { + + /** + * Data decoder function. + * + * @param message client message. + * @param dataType data type class. + * @return client message object. + * @throws MessageCodecException in case if data decoding fails. + */ + default ServiceMessage decodeData(ServiceMessage message, Type dataType) + throws MessageCodecException { + return ServiceMessageCodec.decodeData(message, dataType); + } + + /** + * Encodes {@link ServiceMessage}. + * + * @param message message to encode + * @return encoded message + */ + ByteBuf encode(ServiceMessage message); + + /** + * Decodes {@link ServiceMessage} object from {@link ByteBuf}. + * + * @param byteBuf message to decode + * @return decoded message represented by {@link ServiceMessage} + */ + ServiceMessage decode(ByteBuf byteBuf); +} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/ServiceMessageCodec.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/ServiceMessageCodec.java similarity index 96% rename from services-gateway/src/main/java/io/scalecube/services/gateway/transport/ServiceMessageCodec.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/client/ServiceMessageCodec.java index 8661df8fc..9e6de7b78 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/ServiceMessageCodec.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/ServiceMessageCodec.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.transport; +package io.scalecube.services.gateway.client; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/StaticAddressRouter.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/StaticAddressRouter.java similarity index 75% rename from services-gateway/src/main/java/io/scalecube/services/gateway/transport/StaticAddressRouter.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/client/StaticAddressRouter.java index 43ba4654a..e4216b536 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/StaticAddressRouter.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/StaticAddressRouter.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.transport; +package io.scalecube.services.gateway.client; import io.scalecube.services.Address; import io.scalecube.services.ServiceEndpoint; @@ -12,10 +12,13 @@ import java.util.Optional; import java.util.UUID; -/** Syntethic router for returning preconstructed static service reference with given address. */ -public class StaticAddressRouter implements Router { +/** + * Syntethic router for returning pre-constructed {@link ServiceReference} instance with given + * address. + */ +public final class StaticAddressRouter implements Router { - private final ServiceReference staticServiceReference; + private final ServiceReference serviceReference; /** * Constructor. @@ -23,7 +26,7 @@ public class StaticAddressRouter implements Router { * @param address address */ public StaticAddressRouter(Address address) { - this.staticServiceReference = + serviceReference = new ServiceReference( new ServiceMethodDefinition(UUID.randomUUID().toString()), new ServiceRegistration( @@ -33,6 +36,6 @@ public StaticAddressRouter(Address address) { @Override public Optional route(ServiceRegistry serviceRegistry, ServiceMessage request) { - return Optional.of(staticServiceReference); + return Optional.of(serviceReference); } } diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/http/HttpGatewayClientCodec.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientCodec.java similarity index 78% rename from services-gateway/src/main/java/io/scalecube/services/gateway/transport/http/HttpGatewayClientCodec.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientCodec.java index c0231825e..9a899a8d6 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/http/HttpGatewayClientCodec.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientCodec.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.transport.http; +package io.scalecube.services.gateway.client.http; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -6,14 +6,10 @@ import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.exceptions.MessageCodecException; import io.scalecube.services.gateway.ReferenceCountUtil; -import io.scalecube.services.gateway.transport.GatewayClientCodec; +import io.scalecube.services.gateway.client.GatewayClientCodec; import io.scalecube.services.transport.api.DataCodec; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public final class HttpGatewayClientCodec implements GatewayClientCodec { - - private static final Logger LOGGER = LoggerFactory.getLogger(HttpGatewayClientCodec.class); +public final class HttpGatewayClientCodec implements GatewayClientCodec { private final DataCodec dataCodec; @@ -38,7 +34,6 @@ public ByteBuf encode(ServiceMessage message) { dataCodec.encode(new ByteBufOutputStream(content), message.data()); } catch (Throwable t) { ReferenceCountUtil.safestRelease(content); - LOGGER.error("Failed to encode data on: {}, cause: {}", message, t); throw new MessageCodecException( "Failed to encode data on message q=" + message.qualifier(), t); } diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java new file mode 100644 index 000000000..ae7655f35 --- /dev/null +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/http/HttpGatewayClientTransport.java @@ -0,0 +1,192 @@ +package io.scalecube.services.gateway.client.http; + +import static io.scalecube.services.gateway.client.ServiceMessageCodec.decodeData; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelOption; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.scalecube.services.Address; +import io.scalecube.services.ServiceReference; +import io.scalecube.services.api.ServiceMessage; +import io.scalecube.services.gateway.client.GatewayClientCodec; +import io.scalecube.services.transport.api.ClientChannel; +import io.scalecube.services.transport.api.ClientTransport; +import io.scalecube.services.transport.api.DataCodec; +import java.lang.reflect.Type; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.NettyOutbound; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientRequest; +import reactor.netty.http.client.HttpClientResponse; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.resources.LoopResources; + +public final class HttpGatewayClientTransport implements ClientChannel, ClientTransport { + + private static final Logger LOGGER = LoggerFactory.getLogger(HttpGatewayClientTransport.class); + + private static final String CONTENT_TYPE = "application/json"; + private static final HttpGatewayClientCodec CLIENT_CODEC = + new HttpGatewayClientCodec(DataCodec.getInstance(CONTENT_TYPE)); + private static final int CONNECT_TIMEOUT_MILLIS = (int) Duration.ofSeconds(5).toMillis(); + + private final GatewayClientCodec clientCodec; + private final LoopResources loopResources; + private final Function operator; + private final boolean ownsLoopResources; + + private final AtomicReference httpClientReference = new AtomicReference<>(); + + private HttpGatewayClientTransport(Builder builder) { + this.clientCodec = builder.clientCodec; + this.operator = builder.operator; + this.loopResources = + builder.loopResources == null + ? LoopResources.create("http-gateway-client", 1, true) + : builder.loopResources; + this.ownsLoopResources = builder.loopResources == null; + } + + @Override + public ClientChannel create(ServiceReference serviceReference) { + httpClientReference.getAndUpdate( + oldValue -> { + if (oldValue != null) { + return oldValue; + } + + return operator.apply( + HttpClient.create(ConnectionProvider.create("http-gateway-client")) + .runOn(loopResources) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MILLIS) + .option(ChannelOption.TCP_NODELAY, true) + .headers(headers -> headers.set(HttpHeaderNames.CONTENT_TYPE, CONTENT_TYPE))); + }); + return this; + } + + @Override + public Mono requestResponse(ServiceMessage request, Type responseType) { + return Mono.defer( + () -> { + final HttpClient httpClient = httpClientReference.get(); + return httpClient + .post() + .uri("/" + request.qualifier()) + .send((clientRequest, outbound) -> send(request, clientRequest, outbound)) + .responseSingle( + (clientResponse, mono) -> + mono.map(ByteBuf::retain).map(data -> toMessage(clientResponse, data))) + .map(msg -> decodeData(msg, responseType)); + }); + } + + private Mono send( + ServiceMessage request, HttpClientRequest clientRequest, NettyOutbound outbound) { + LOGGER.debug("Sending request: {}", request); + // prepare request headers + request.headers().forEach(clientRequest::header); + // send with publisher (defer buffer cleanup to netty) + return outbound.sendObject(Mono.just(clientCodec.encode(request))).then(); + } + + @Override + public Flux requestStream(ServiceMessage message, Type responseType) { + return Flux.error(new UnsupportedOperationException("requestStream is not supported")); + } + + @Override + public Flux requestChannel( + Publisher publisher, Type responseType) { + return Flux.error(new UnsupportedOperationException("requestChannel is not supported")); + } + + private static ServiceMessage toMessage(HttpClientResponse httpResponse, ByteBuf data) { + ServiceMessage.Builder builder = + ServiceMessage.builder().qualifier(httpResponse.uri()).data(data); + + HttpResponseStatus status = httpResponse.status(); + if (isError(status)) { + builder.header(ServiceMessage.HEADER_ERROR_TYPE, status.code()); + } + + // prepare response headers + httpResponse + .responseHeaders() + .entries() + .forEach(entry -> builder.header(entry.getKey(), entry.getValue())); + ServiceMessage message = builder.build(); + + LOGGER.debug("Received response: {}", message); + return message; + } + + private static boolean isError(HttpResponseStatus status) { + return status.code() >= 400 && status.code() <= 599; + } + + @Override + public void close() { + if (ownsLoopResources) { + loopResources.dispose(); + } + } + + public static class Builder { + + private GatewayClientCodec clientCodec = CLIENT_CODEC; + private LoopResources loopResources; + private Function operator = client -> client; + + public Builder() {} + + public Builder clientCodec(GatewayClientCodec clientCodec) { + this.clientCodec = clientCodec; + return this; + } + + public Builder loopResources(LoopResources loopResources) { + this.loopResources = loopResources; + return this; + } + + public Builder httpClient(UnaryOperator operator) { + this.operator = this.operator.andThen(operator); + return this; + } + + public Builder address(Address address) { + return httpClient(client -> client.host(address.host()).port(address.port())); + } + + public Builder connectTimeout(Duration connectTimeout) { + return httpClient( + client -> + client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectTimeout.toMillis())); + } + + public Builder contentType(String contentType) { + return httpClient( + client -> + client.headers(headers -> headers.set(HttpHeaderNames.CONTENT_TYPE, contentType))); + } + + public Builder headers(Map headers) { + return httpClient(client -> client.headers(entries -> headers.forEach(entries::set))); + } + + public HttpGatewayClientTransport build() { + return new HttpGatewayClientTransport(this); + } + } +} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/Signal.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/Signal.java similarity index 93% rename from services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/Signal.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/Signal.java index ccb7bd1d3..e987598d7 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/Signal.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/Signal.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.transport.websocket; +package io.scalecube.services.gateway.client.websocket; public enum Signal { COMPLETE(1), diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClientCodec.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientCodec.java similarity index 94% rename from services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClientCodec.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientCodec.java index 087edab14..30f4df595 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClientCodec.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientCodec.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.transport.websocket; +package io.scalecube.services.gateway.client.websocket; import static com.fasterxml.jackson.core.JsonToken.VALUE_NULL; @@ -21,17 +21,13 @@ import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.exceptions.MessageCodecException; import io.scalecube.services.gateway.ReferenceCountUtil; -import io.scalecube.services.gateway.transport.GatewayClientCodec; +import io.scalecube.services.gateway.client.GatewayClientCodec; import java.io.InputStream; import java.io.OutputStream; import java.util.Map.Entry; import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public final class WebsocketGatewayClientCodec implements GatewayClientCodec { - - private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketGatewayClientCodec.class); +public final class WebsocketGatewayClientCodec implements GatewayClientCodec { private static final MappingJsonFactory jsonFactory = new MappingJsonFactory(objectMapper()); @@ -104,7 +100,6 @@ public ByteBuf encode(ServiceMessage message) { } catch (Throwable ex) { ReferenceCountUtil.safestRelease(byteBuf); Optional.ofNullable(message.data()).ifPresent(ReferenceCountUtil::safestRelease); - LOGGER.error("Failed to encode message: {}", message, ex); throw new MessageCodecException("Failed to encode message", ex); } return byteBuf; diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClientSession.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientSession.java similarity index 93% rename from services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClientSession.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientSession.java index f75a7c9e6..24c637c35 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClientSession.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientSession.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.transport.websocket; +package io.scalecube.services.gateway.client.websocket; import static reactor.core.publisher.Sinks.EmitFailureHandler.busyLooping; @@ -7,7 +7,7 @@ import io.scalecube.services.api.ErrorData; import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.gateway.ReferenceCountUtil; -import io.scalecube.services.gateway.transport.GatewayClientCodec; +import io.scalecube.services.gateway.client.GatewayClientCodec; import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.Map; @@ -34,15 +34,15 @@ public final class WebsocketGatewayClientSession { private static final String SIGNAL = "sig"; private final String id; // keep id for tracing - private final GatewayClientCodec codec; + private final GatewayClientCodec clientCodec; private final Connection connection; // processor by sid mapping private final Map inboundProcessors = new NonBlockingHashMapLong<>(1024); - WebsocketGatewayClientSession(GatewayClientCodec codec, Connection connection) { + WebsocketGatewayClientSession(GatewayClientCodec clientCodec, Connection connection) { this.id = Integer.toHexString(System.identityHashCode(this)); - this.codec = codec; + this.clientCodec = clientCodec; this.connection = connection; WebsocketInbound inbound = (WebsocketInbound) connection.inbound(); @@ -59,7 +59,7 @@ public final class WebsocketGatewayClientSession { // decode message ServiceMessage message; try { - message = codec.decode(byteBuf); + message = clientCodec.decode(byteBuf); } catch (Exception ex) { LOGGER.error("Response decoder failed:", ex); return; @@ -124,7 +124,7 @@ Mono send(ByteBuf byteBuf) { void cancel(long sid, String qualifier) { ByteBuf byteBuf = - codec.encode( + clientCodec.encode( ServiceMessage.builder() .qualifier(qualifier) .header(STREAM_ID, sid) @@ -177,7 +177,7 @@ private void handleResponse(ServiceMessage response, Object processor) { } if (signal == Signal.ERROR) { // decode error data to retrieve real error cause - emitNext(processor, codec.decodeData(response, ErrorData.class)); + emitNext(processor, clientCodec.decodeData(response, ErrorData.class)); } } } catch (Exception e) { diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientTransport.java b/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientTransport.java new file mode 100644 index 000000000..a9a892b0a --- /dev/null +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/client/websocket/WebsocketGatewayClientTransport.java @@ -0,0 +1,253 @@ +package io.scalecube.services.gateway.client.websocket; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelOption; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.scalecube.services.Address; +import io.scalecube.services.ServiceReference; +import io.scalecube.services.api.ServiceMessage; +import io.scalecube.services.gateway.client.GatewayClientCodec; +import io.scalecube.services.gateway.client.ServiceMessageCodec; +import io.scalecube.services.transport.api.ClientChannel; +import io.scalecube.services.transport.api.ClientTransport; +import java.lang.reflect.Type; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.http.client.HttpClient; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.resources.LoopResources; + +public final class WebsocketGatewayClientTransport implements ClientChannel, ClientTransport { + + private static final Logger LOGGER = + LoggerFactory.getLogger(WebsocketGatewayClientTransport.class); + + private static final String STREAM_ID = "sid"; + private static final String CONTENT_TYPE = "application/json"; + private static final WebsocketGatewayClientCodec CLIENT_CODEC = new WebsocketGatewayClientCodec(); + private static final int CONNECT_TIMEOUT_MILLIS = (int) Duration.ofSeconds(5).toMillis(); + + private final GatewayClientCodec clientCodec; + private final LoopResources loopResources; + private final Duration keepAliveInterval; + private final Function operator; + private final boolean ownsLoopResources; + + private final AtomicLong sidCounter = new AtomicLong(); + private final AtomicReference clientSessionReference = + new AtomicReference<>(); + + private WebsocketGatewayClientTransport(Builder builder) { + this.clientCodec = builder.clientCodec; + this.keepAliveInterval = builder.keepAliveInterval; + this.operator = builder.operator; + this.loopResources = + builder.loopResources == null + ? LoopResources.create("websocket-gateway-client", 1, true) + : builder.loopResources; + this.ownsLoopResources = builder.loopResources == null; + } + + @Override + public ClientChannel create(ServiceReference serviceReference) { + clientSessionReference.getAndUpdate( + oldValue -> { + if (oldValue != null) { + return oldValue; + } + + final HttpClient httpClient = + operator.apply( + HttpClient.create(ConnectionProvider.newConnection()) + .runOn(loopResources) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MILLIS) + .option(ChannelOption.TCP_NODELAY, true) + .headers(headers -> headers.set(HttpHeaderNames.CONTENT_TYPE, CONTENT_TYPE))); + + return clientSession(httpClient); + }); + return this; + } + + private WebsocketGatewayClientSession clientSession(HttpClient httpClient) { + try { + return httpClient + .websocket() + .uri("/") + .connect() + .map( + connection -> + keepAliveInterval != Duration.ZERO + ? connection + .onReadIdle(keepAliveInterval.toMillis(), () -> onReadIdle(connection)) + .onWriteIdle(keepAliveInterval.toMillis(), () -> onWriteIdle(connection)) + : connection) + .map( + connection -> { + final WebsocketGatewayClientSession session = + new WebsocketGatewayClientSession(clientCodec, connection); + LOGGER.info("Created session: {}", session); + // setup shutdown hook + session + .onClose() + .doOnTerminate(() -> LOGGER.info("Closed session: {}", session)) + .subscribe( + null, + th -> + LOGGER.warn( + "Exception on closing session: {}, cause: {}", + session, + th.toString())); + return session; + }) + .doOnError(ex -> LOGGER.warn("Failed to connect, cause: {}", ex.toString())) + .toFuture() + .get(); + } catch (Exception e) { + throw new RuntimeException(getRootCause(e)); + } + } + + @Override + public Mono requestResponse(ServiceMessage request, Type responseType) { + return Mono.defer( + () -> { + long sid = sidCounter.incrementAndGet(); + final WebsocketGatewayClientSession session = clientSessionReference.get(); + return session + .send(encodeRequest(request, sid)) + .doOnSubscribe(s -> LOGGER.debug("Sending request {}", request)) + .then(session.newMonoProcessor(sid).asMono()) + .map(msg -> ServiceMessageCodec.decodeData(msg, responseType)) + .doOnCancel(() -> session.cancel(sid, request.qualifier())) + .doFinally(s -> session.removeProcessor(sid)); + }); + } + + @Override + public Flux requestStream(ServiceMessage request, Type responseType) { + return Flux.defer( + () -> { + long sid = sidCounter.incrementAndGet(); + final WebsocketGatewayClientSession session = clientSessionReference.get(); + return session + .send(encodeRequest(request, sid)) + .doOnSubscribe(s -> LOGGER.debug("Sending request {}", request)) + .thenMany(session.newUnicastProcessor(sid).asFlux()) + .map(msg -> ServiceMessageCodec.decodeData(msg, responseType)) + .doOnCancel(() -> session.cancel(sid, request.qualifier())) + .doFinally(s -> session.removeProcessor(sid)); + }); + } + + @Override + public Flux requestChannel( + Publisher publisher, Type responseType) { + return Flux.error(new UnsupportedOperationException("requestChannel is not supported")); + } + + private static void onWriteIdle(Connection connection) { + connection + .outbound() + .sendObject(new PingWebSocketFrame()) + .then() + .subscribe( + null, + ex -> { + // no-op + }); + } + + private static void onReadIdle(Connection connection) { + connection + .outbound() + .sendObject(new PingWebSocketFrame()) + .then() + .subscribe( + null, + ex -> { + // no-op + }); + } + + private ByteBuf encodeRequest(ServiceMessage message, long sid) { + return clientCodec.encode(ServiceMessage.from(message).header(STREAM_ID, sid).build()); + } + + private static Throwable getRootCause(Throwable throwable) { + Throwable cause = throwable.getCause(); + return (cause == null) ? throwable : getRootCause(cause); + } + + @Override + public void close() { + if (ownsLoopResources) { + loopResources.dispose(); + } + } + + public static class Builder { + + private GatewayClientCodec clientCodec = CLIENT_CODEC; + private LoopResources loopResources; + private Duration keepAliveInterval = Duration.ZERO; + private Function operator = client -> client; + + public Builder() {} + + public Builder clientCodec(GatewayClientCodec clientCodec) { + this.clientCodec = clientCodec; + return this; + } + + public Builder loopResources(LoopResources loopResources) { + this.loopResources = loopResources; + return this; + } + + public Builder httpClient(UnaryOperator operator) { + this.operator = this.operator.andThen(operator); + return this; + } + + public Builder address(Address address) { + return httpClient(client -> client.host(address.host()).port(address.port())); + } + + public Builder connectTimeout(Duration connectTimeout) { + return httpClient( + client -> + client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectTimeout.toMillis())); + } + + public Builder contentType(String contentType) { + return httpClient( + client -> + client.headers(headers -> headers.set(HttpHeaderNames.CONTENT_TYPE, contentType))); + } + + public Builder keepAliveInterval(Duration keepAliveInterval) { + this.keepAliveInterval = keepAliveInterval; + return this; + } + + public Builder headers(Map headers) { + return httpClient(client -> client.headers(entries -> headers.forEach(entries::set))); + } + + public WebsocketGatewayClientTransport build() { + return new WebsocketGatewayClientTransport(this); + } + } +} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGateway.java b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGateway.java index d1c9a630b..272232b5c 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGateway.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGateway.java @@ -1,7 +1,6 @@ package io.scalecube.services.gateway.http; import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.cors.CorsConfig; import io.netty.handler.codec.http.cors.CorsConfigBuilder; import io.netty.handler.codec.http.cors.CorsHandler; import io.scalecube.services.Address; @@ -9,140 +8,55 @@ import io.scalecube.services.exceptions.ServiceProviderErrorMapper; import io.scalecube.services.gateway.Gateway; import io.scalecube.services.gateway.GatewayOptions; -import io.scalecube.services.gateway.GatewayTemplate; import java.net.InetSocketAddress; -import java.util.Map.Entry; import java.util.StringJoiner; -import java.util.function.UnaryOperator; +import java.util.function.Consumer; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; import reactor.netty.resources.LoopResources; -public class HttpGateway extends GatewayTemplate { +public class HttpGateway implements Gateway { + private final GatewayOptions options; private final ServiceProviderErrorMapper errorMapper; + private final boolean corsEnabled; + private final CorsConfigBuilder corsConfigBuilder; private DisposableServer server; private LoopResources loopResources; - private boolean corsEnabled = false; - private CorsConfigBuilder corsConfigBuilder = - CorsConfigBuilder.forAnyOrigin() - .allowNullOrigin() - .maxAge(3600) - .allowedRequestMethods(HttpMethod.POST); - - public HttpGateway(GatewayOptions options) { - this(options, DefaultErrorMapper.INSTANCE); - } - - public HttpGateway(GatewayOptions options, ServiceProviderErrorMapper errorMapper) { - super(options); - this.errorMapper = errorMapper; + private HttpGateway(Builder builder) { + this.options = builder.options; + this.errorMapper = builder.errorMapper; + this.corsEnabled = builder.corsEnabled; + this.corsConfigBuilder = builder.corsConfigBuilder; } - private HttpGateway(HttpGateway other) { - super(other.options); - this.server = other.server; - this.loopResources = other.loopResources; - this.corsEnabled = other.corsEnabled; - this.corsConfigBuilder = copy(other.corsConfigBuilder); - this.errorMapper = other.errorMapper; - } - - /** - * CORS enable. - * - * @param corsEnabled if set to true. - * @return HttpGateway with CORS settings. - */ - public HttpGateway corsEnabled(boolean corsEnabled) { - HttpGateway g = new HttpGateway(this); - g.corsEnabled = corsEnabled; - return g; - } - - /** - * Configure CORS with options. - * - * @param op for CORS. - * @return HttpGateway with CORS settings. - */ - public HttpGateway corsConfig(UnaryOperator op) { - HttpGateway g = new HttpGateway(this); - g.corsConfigBuilder = copy(op.apply(g.corsConfigBuilder)); - return g; - } - - private CorsConfigBuilder copy(CorsConfigBuilder other) { - CorsConfig config = other.build(); - CorsConfigBuilder corsConfigBuilder; - if (config.isAnyOriginSupported()) { - corsConfigBuilder = CorsConfigBuilder.forAnyOrigin(); - } else { - corsConfigBuilder = CorsConfigBuilder.forOrigins(config.origins().toArray(new String[0])); - } - - if (!config.isCorsSupportEnabled()) { - corsConfigBuilder.disable(); - } - - corsConfigBuilder - .exposeHeaders(config.exposedHeaders().toArray(new String[0])) - .allowedRequestHeaders(config.allowedRequestHeaders().toArray(new String[0])) - .allowedRequestMethods(config.allowedRequestMethods().toArray(new HttpMethod[0])) - .maxAge(config.maxAge()); - - for (Entry header : config.preflightResponseHeaders()) { - corsConfigBuilder.preflightResponseHeader(header.getKey(), header.getValue()); - } - - if (config.isShortCircuit()) { - corsConfigBuilder.shortCircuit(); - } - - if (config.isNullOriginAllowed()) { - corsConfigBuilder.allowNullOrigin(); - } - - if (config.isCredentialsAllowed()) { - corsConfigBuilder.allowCredentials(); - } - - return corsConfigBuilder; + @Override + public String id() { + return options.id(); } @Override public Mono start() { return Mono.defer( () -> { - HttpGatewayAcceptor acceptor = new HttpGatewayAcceptor(options.call(), errorMapper); + HttpGatewayAcceptor gatewayAcceptor = + new HttpGatewayAcceptor(options.call(), errorMapper); - loopResources = LoopResources.create("http-gateway"); + loopResources = LoopResources.create(options.id() + ":" + options.port()); return prepareHttpServer(loopResources, options.port()) - .handle(acceptor) + .handle(gatewayAcceptor) .bind() .doOnSuccess(server -> this.server = server) .thenReturn(this); }); } - @Override - public Address address() { - InetSocketAddress address = (InetSocketAddress) server.address(); - return Address.create(address.getHostString(), address.getPort()); - } - - @Override - public Mono stop() { - return Flux.concatDelayError(shutdownServer(server), shutdownLoopResources(loopResources)) - .then(); - } - - protected HttpServer prepareHttpServer(LoopResources loopResources, int port) { + private HttpServer prepareHttpServer(LoopResources loopResources, int port) { HttpServer httpServer = HttpServer.create(); if (loopResources != null) { @@ -159,14 +73,101 @@ protected HttpServer prepareHttpServer(LoopResources loopResources, int port) { }); } + @Override + public Address address() { + InetSocketAddress address = (InetSocketAddress) server.address(); + return Address.create(address.getHostString(), address.getPort()); + } + + @Override + public Mono stop() { + return Flux.concatDelayError(shutdownServer(server), shutdownLoopResources(loopResources)) + .then(); + } + + private Mono shutdownServer(DisposableServer server) { + return Mono.defer( + () -> { + if (server != null) { + server.dispose(); + return server.onDispose(); + } + return Mono.empty(); + }); + } + + private Mono shutdownLoopResources(LoopResources loopResources) { + return loopResources != null ? loopResources.disposeLater() : Mono.empty(); + } + @Override public String toString() { return new StringJoiner(", ", HttpGateway.class.getSimpleName() + "[", "]") - .add("server=" + server) - .add("loopResources=" + loopResources) + .add("options=" + options) + .add("errorMapper=" + errorMapper) .add("corsEnabled=" + corsEnabled) .add("corsConfigBuilder=" + corsConfigBuilder) - .add("options=" + options) + .add("server=" + server) + .add("loopResources=" + loopResources) .toString(); } + + public static class Builder { + + private GatewayOptions options; + private ServiceProviderErrorMapper errorMapper = DefaultErrorMapper.INSTANCE; + private boolean corsEnabled = false; + private CorsConfigBuilder corsConfigBuilder = + CorsConfigBuilder.forAnyOrigin() + .allowNullOrigin() + .maxAge(3600) + .allowedRequestMethods(HttpMethod.POST); + + public Builder() {} + + public GatewayOptions options() { + return options; + } + + public Builder options(GatewayOptions options) { + this.options = options; + return this; + } + + public ServiceProviderErrorMapper errorMapper() { + return errorMapper; + } + + public Builder errorMapper(ServiceProviderErrorMapper errorMapper) { + this.errorMapper = errorMapper; + return this; + } + + public boolean corsEnabled() { + return corsEnabled; + } + + public Builder corsEnabled(boolean corsEnabled) { + this.corsEnabled = corsEnabled; + return this; + } + + public CorsConfigBuilder corsConfigBuilder() { + return corsConfigBuilder; + } + + public Builder corsConfigBuilder(CorsConfigBuilder corsConfigBuilder) { + this.corsConfigBuilder = corsConfigBuilder; + return this; + } + + public Builder corsConfigBuilder(Consumer consumer) { + consumer.accept(this.corsConfigBuilder); + return this; + } + + public HttpGateway build() { + return new HttpGateway(this); + } + } } diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java index 115623fa8..3a04f22de 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/http/HttpGatewayAcceptor.java @@ -55,7 +55,6 @@ public Publisher apply(HttpServerRequest httpRequest, HttpServerResponse h httpRequest.params()); if (httpRequest.method() != POST) { - LOGGER.error("Unsupported HTTP method. Expected POST, actual {}", httpRequest.method()); return methodNotAllowed(httpResponse); } diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClient.java b/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClient.java deleted file mode 100644 index eac90f453..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClient.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.scalecube.services.gateway.transport; - -import io.scalecube.services.api.ServiceMessage; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public interface GatewayClient { - - /** - * Communication mode that gives single response to single request. - * - * @param request request message. - * @return Publisher that emits single response form remote server as it's ready. - */ - Mono requestResponse(ServiceMessage request); - - /** - * Communication mode that gives stream of responses to single request. - * - * @param request request message. - * @return Publisher that emits responses from remote server. - */ - Flux requestStream(ServiceMessage request); - - /** - * Communication mode that gives stream of responses to stream of requests. - * - * @param requests request stream. - * @return Publisher that emits responses from remote server. - */ - Flux requestChannel(Flux requests); - - /** - * Initiate cleaning of underlying resources (if any) like closing websocket connection or rSocket - * session. Subsequent calls of requestOne() or requestMany() must issue new connection creation. - * Note that close is not the end of client lifecycle. - */ - void close(); - - /** - * Return close completion signal of the gateway client. - * - * @return close completion signal - */ - Mono onClose(); -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientChannel.java b/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientChannel.java deleted file mode 100644 index bb8c72c65..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientChannel.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.scalecube.services.gateway.transport; - -import io.scalecube.services.api.ServiceMessage; -import io.scalecube.services.transport.api.ClientChannel; -import java.lang.reflect.Type; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public class GatewayClientChannel implements ClientChannel { - - private final GatewayClient gatewayClient; - - GatewayClientChannel(GatewayClient gatewayClient) { - this.gatewayClient = gatewayClient; - } - - @Override - public Mono requestResponse(ServiceMessage clientMessage, Type responseType) { - return gatewayClient - .requestResponse(clientMessage) - .map(msg -> ServiceMessageCodec.decodeData(msg, responseType)); - } - - @Override - public Flux requestStream(ServiceMessage clientMessage, Type responseType) { - return gatewayClient - .requestStream(clientMessage) - .map(msg -> ServiceMessageCodec.decodeData(msg, responseType)); - } - - @Override - public Flux requestChannel( - Publisher clientMessageStream, Type responseType) { - return gatewayClient - .requestChannel(Flux.from(clientMessageStream)) - .map(msg -> ServiceMessageCodec.decodeData(msg, responseType)); - } -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientCodec.java b/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientCodec.java deleted file mode 100644 index 814f8bc95..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientCodec.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.scalecube.services.gateway.transport; - -import io.scalecube.services.api.ServiceMessage; -import io.scalecube.services.exceptions.MessageCodecException; -import java.lang.reflect.Type; - -/** - * Describes encoding/decoding operations for {@link ServiceMessage} to/from {@link T} type. - * - * @param represents source or result for decoding or encoding operations respectively - */ -public interface GatewayClientCodec { - - /** - * Data decoder function. - * - * @param message client message. - * @param dataType data type class. - * @return client message object. - * @throws MessageCodecException in case if data decoding fails. - */ - default ServiceMessage decodeData(ServiceMessage message, Type dataType) - throws MessageCodecException { - return ServiceMessageCodec.decodeData(message, dataType); - } - - /** - * Encodes {@link ServiceMessage} to {@link T} type. - * - * @param message client message to encode - * @return encoded message represented by {@link T} type - */ - T encode(ServiceMessage message); - - /** - * Decodes message represented by {@link T} type to {@link ServiceMessage} object. - * - * @param encodedMessage message to decode - * @return decoded message represented by {@link ServiceMessage} type - */ - ServiceMessage decode(T encodedMessage); -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientSettings.java b/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientSettings.java deleted file mode 100644 index 013a9e910..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientSettings.java +++ /dev/null @@ -1,213 +0,0 @@ -package io.scalecube.services.gateway.transport; - -import io.scalecube.services.Address; -import io.scalecube.services.exceptions.DefaultErrorMapper; -import io.scalecube.services.exceptions.ServiceClientErrorMapper; -import java.time.Duration; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import reactor.netty.tcp.SslProvider; - -public class GatewayClientSettings { - - private static final String DEFAULT_HOST = "localhost"; - private static final String DEFAULT_CONTENT_TYPE = "application/json"; - private static final Duration DEFAULT_KEEPALIVE_INTERVAL = Duration.ZERO; - - private final String host; - private final int port; - private final String contentType; - private final boolean followRedirect; - private final SslProvider sslProvider; - private final ServiceClientErrorMapper errorMapper; - private final Duration keepAliveInterval; - private final boolean wiretap; - private final Map headers; - - private GatewayClientSettings(Builder builder) { - this.host = builder.host; - this.port = builder.port; - this.contentType = builder.contentType; - this.followRedirect = builder.followRedirect; - this.sslProvider = builder.sslProvider; - this.errorMapper = builder.errorMapper; - this.keepAliveInterval = builder.keepAliveInterval; - this.wiretap = builder.wiretap; - this.headers = builder.headers; - } - - public String host() { - return host; - } - - public int port() { - return port; - } - - public String contentType() { - return this.contentType; - } - - public boolean followRedirect() { - return followRedirect; - } - - public SslProvider sslProvider() { - return sslProvider; - } - - public ServiceClientErrorMapper errorMapper() { - return errorMapper; - } - - public Duration keepAliveInterval() { - return this.keepAliveInterval; - } - - public boolean wiretap() { - return this.wiretap; - } - - public Map headers() { - return headers; - } - - public static Builder builder() { - return new Builder(); - } - - public static Builder from(GatewayClientSettings gatewayClientSettings) { - return new Builder(gatewayClientSettings); - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder("GatewayClientSettings{"); - sb.append("host='").append(host).append('\''); - sb.append(", port=").append(port); - sb.append(", contentType='").append(contentType).append('\''); - sb.append(", followRedirect=").append(followRedirect); - sb.append(", keepAliveInterval=").append(keepAliveInterval); - sb.append(", wiretap=").append(wiretap); - sb.append(", sslProvider=").append(sslProvider); - sb.append('}'); - return sb.toString(); - } - - public static class Builder { - - private String host = DEFAULT_HOST; - private int port; - private String contentType = DEFAULT_CONTENT_TYPE; - private boolean followRedirect = true; - private SslProvider sslProvider; - private ServiceClientErrorMapper errorMapper = DefaultErrorMapper.INSTANCE; - private Duration keepAliveInterval = DEFAULT_KEEPALIVE_INTERVAL; - private boolean wiretap = false; - private Map headers = Collections.emptyMap(); - - private Builder() {} - - private Builder(GatewayClientSettings originalSettings) { - this.host = originalSettings.host; - this.port = originalSettings.port; - this.contentType = originalSettings.contentType; - this.followRedirect = originalSettings.followRedirect; - this.sslProvider = originalSettings.sslProvider; - this.errorMapper = originalSettings.errorMapper; - this.keepAliveInterval = originalSettings.keepAliveInterval; - this.wiretap = originalSettings.wiretap; - this.headers = Collections.unmodifiableMap(new HashMap<>(originalSettings.headers)); - } - - public Builder host(String host) { - this.host = host; - return this; - } - - public Builder port(int port) { - this.port = port; - return this; - } - - public Builder address(Address address) { - return host(address.host()).port(address.port()); - } - - public Builder contentType(String contentType) { - this.contentType = contentType; - return this; - } - - /** - * Specifies is auto-redirect enabled for HTTP 301/302 status codes. Enabled by default. - * - * @param followRedirect if true auto-redirect is enabled, otherwise disabled - * @return builder - */ - public Builder followRedirect(boolean followRedirect) { - this.followRedirect = followRedirect; - return this; - } - - /** - * Use default SSL client provider. - * - * @return builder - */ - public Builder secure() { - this.sslProvider = SslProvider.defaultClientProvider(); - return this; - } - - /** - * Use specified SSL provider. - * - * @param sslProvider SSL provider - * @return builder - */ - public Builder secure(SslProvider sslProvider) { - this.sslProvider = sslProvider; - return this; - } - - /** - * Keepalive interval. If client's channel doesn't have any activity at channel during this - * period, it will send a keepalive message to the server. - * - * @param keepAliveInterval keepalive interval. - * @return builder - */ - public Builder keepAliveInterval(Duration keepAliveInterval) { - this.keepAliveInterval = keepAliveInterval; - return this; - } - - /** - * Specifies whether to enaple 'wiretap' option for connections. That logs full netty traffic. - * Default is {@code false} - * - * @param wiretap whether to enable 'wiretap' handler at connection. Default - false - * @return builder - */ - public Builder wiretap(boolean wiretap) { - this.wiretap = wiretap; - return this; - } - - public Builder errorMapper(ServiceClientErrorMapper errorMapper) { - this.errorMapper = errorMapper; - return this; - } - - public Builder headers(Map headers) { - this.headers = Collections.unmodifiableMap(new HashMap<>(headers)); - return this; - } - - public GatewayClientSettings build() { - return new GatewayClientSettings(this); - } - } -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientTransport.java b/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientTransport.java deleted file mode 100644 index c8b9e6010..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientTransport.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.scalecube.services.gateway.transport; - -import io.scalecube.services.ServiceReference; -import io.scalecube.services.transport.api.ClientChannel; -import io.scalecube.services.transport.api.ClientTransport; - -public class GatewayClientTransport implements ClientTransport { - - private final GatewayClient gatewayClient; - - public GatewayClientTransport(GatewayClient gatewayClient) { - this.gatewayClient = gatewayClient; - } - - @Override - public ClientChannel create(ServiceReference serviceReference) { - return new GatewayClientChannel(gatewayClient); - } -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientTransports.java b/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientTransports.java deleted file mode 100644 index f5e7d583d..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/GatewayClientTransports.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.scalecube.services.gateway.transport; - -import io.scalecube.services.gateway.transport.http.HttpGatewayClient; -import io.scalecube.services.gateway.transport.http.HttpGatewayClientCodec; -import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClient; -import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClientCodec; -import io.scalecube.services.transport.api.ClientTransport; -import io.scalecube.services.transport.api.DataCodec; -import java.util.function.Function; - -public class GatewayClientTransports { - - private static final String CONTENT_TYPE = "application/json"; - - public static final WebsocketGatewayClientCodec WEBSOCKET_CLIENT_CODEC = - new WebsocketGatewayClientCodec(); - - public static final HttpGatewayClientCodec HTTP_CLIENT_CODEC = - new HttpGatewayClientCodec(DataCodec.getInstance(CONTENT_TYPE)); - - private GatewayClientTransports() { - // utils - } - - /** - * ClientTransport that is capable of communicating with Gateway over websocket. - * - * @param cs client settings for gateway client transport - * @return client transport - */ - public static ClientTransport websocketGatewayClientTransport(GatewayClientSettings cs) { - final Function function = - settings -> new WebsocketGatewayClient(settings, WEBSOCKET_CLIENT_CODEC); - return new GatewayClientTransport(function.apply(cs)); - } - - /** - * ClientTransport that is capable of communicating with Gateway over http. - * - * @param cs client settings for gateway client transport - * @return client transport - */ - public static ClientTransport httpGatewayClientTransport(GatewayClientSettings cs) { - final Function function = - settings -> new HttpGatewayClient(settings, HTTP_CLIENT_CODEC); - return new GatewayClientTransport(function.apply(cs)); - } -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/http/HttpGatewayClient.java b/services-gateway/src/main/java/io/scalecube/services/gateway/transport/http/HttpGatewayClient.java deleted file mode 100644 index 5f827604b..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/http/HttpGatewayClient.java +++ /dev/null @@ -1,166 +0,0 @@ -package io.scalecube.services.gateway.transport.http; - -import static reactor.core.publisher.Sinks.EmitFailureHandler.busyLooping; - -import io.netty.buffer.ByteBuf; -import io.scalecube.services.api.ServiceMessage; -import io.scalecube.services.api.ServiceMessage.Builder; -import io.scalecube.services.gateway.transport.GatewayClient; -import io.scalecube.services.gateway.transport.GatewayClientCodec; -import io.scalecube.services.gateway.transport.GatewayClientSettings; -import java.time.Duration; -import java.util.function.BiFunction; -import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; -import reactor.netty.NettyOutbound; -import reactor.netty.http.client.HttpClient; -import reactor.netty.http.client.HttpClientRequest; -import reactor.netty.http.client.HttpClientResponse; -import reactor.netty.resources.ConnectionProvider; -import reactor.netty.resources.LoopResources; - -public final class HttpGatewayClient implements GatewayClient { - - private static final Logger LOGGER = LoggerFactory.getLogger(HttpGatewayClient.class); - - private final GatewayClientCodec codec; - private final HttpClient httpClient; - private final LoopResources loopResources; - private final boolean ownsLoopResources; - - private final Sinks.One close = Sinks.one(); - private final Sinks.One onClose = Sinks.one(); - - /** - * Constructor. - * - * @param settings settings - * @param codec codec - */ - public HttpGatewayClient(GatewayClientSettings settings, GatewayClientCodec codec) { - this(settings, codec, LoopResources.create("http-gateway-client"), true); - } - - /** - * Constructor. - * - * @param settings settings - * @param codec codec - * @param loopResources loopResources - */ - public HttpGatewayClient( - GatewayClientSettings settings, - GatewayClientCodec codec, - LoopResources loopResources) { - this(settings, codec, loopResources, false); - } - - private HttpGatewayClient( - GatewayClientSettings settings, - GatewayClientCodec codec, - LoopResources loopResources, - boolean ownsLoopResources) { - - this.codec = codec; - this.loopResources = loopResources; - this.ownsLoopResources = ownsLoopResources; - - HttpClient httpClient = - HttpClient.create(ConnectionProvider.create("http-gateway-client")) - .headers(headers -> settings.headers().forEach(headers::add)) - .followRedirect(settings.followRedirect()) - .wiretap(settings.wiretap()) - .runOn(loopResources) - .host(settings.host()) - .port(settings.port()); - - if (settings.sslProvider() != null) { - httpClient = httpClient.secure(settings.sslProvider()); - } - - this.httpClient = httpClient; - - // Setup cleanup - close - .asMono() - .then(doClose()) - .doFinally(s -> onClose.emitEmpty(busyLooping(Duration.ofSeconds(3)))) - .doOnTerminate(() -> LOGGER.info("Closed HttpGatewayClient resources")) - .subscribe(null, ex -> LOGGER.warn("Exception occurred on HttpGatewayClient close: " + ex)); - } - - @Override - public Mono requestResponse(ServiceMessage request) { - return Mono.defer( - () -> { - BiFunction> sender = - (httpRequest, out) -> { - LOGGER.debug("Sending request {}", request); - // prepare request headers - request.headers().forEach(httpRequest::header); - // send with publisher (defer buffer cleanup to netty) - return out.sendObject(Mono.just(codec.encode(request))).then(); - }; - return httpClient - .post() - .uri("/" + request.qualifier()) - .send(sender) - .responseSingle( - (httpResponse, bbMono) -> - bbMono.map(ByteBuf::retain).map(content -> toMessage(httpResponse, content))); - }); - } - - @Override - public Flux requestStream(ServiceMessage request) { - return Flux.error( - new UnsupportedOperationException("requestStream is not supported by HTTP/1.x")); - } - - @Override - public Flux requestChannel(Flux requests) { - return Flux.error( - new UnsupportedOperationException("requestChannel is not supported by HTTP/1.x")); - } - - @Override - public void close() { - close.emitEmpty(busyLooping(Duration.ofSeconds(3))); - } - - @Override - public Mono onClose() { - return onClose.asMono(); - } - - private Mono doClose() { - return ownsLoopResources ? Mono.defer(loopResources::disposeLater) : Mono.empty(); - } - - private ServiceMessage toMessage(HttpClientResponse httpResponse, ByteBuf content) { - Builder builder = ServiceMessage.builder().qualifier(httpResponse.uri()).data(content); - - int httpCode = httpResponse.status().code(); - if (isError(httpCode)) { - builder.header(ServiceMessage.HEADER_ERROR_TYPE, String.valueOf(httpCode)); - } - - // prepare response headers - httpResponse - .responseHeaders() - .entries() - .forEach(entry -> builder.header(entry.getKey(), entry.getValue())); - ServiceMessage message = builder.build(); - - LOGGER.debug("Received response {}", message); - return message; - } - - private boolean isError(int httpCode) { - return httpCode >= 400 && httpCode <= 599; - } -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClient.java b/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClient.java deleted file mode 100644 index ff6afab2f..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/transport/websocket/WebsocketGatewayClient.java +++ /dev/null @@ -1,234 +0,0 @@ -package io.scalecube.services.gateway.transport.websocket; - -import static reactor.core.publisher.Sinks.EmitFailureHandler.busyLooping; - -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; -import io.scalecube.services.api.ServiceMessage; -import io.scalecube.services.gateway.transport.GatewayClient; -import io.scalecube.services.gateway.transport.GatewayClientCodec; -import io.scalecube.services.gateway.transport.GatewayClientSettings; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; -import reactor.netty.Connection; -import reactor.netty.http.client.HttpClient; -import reactor.netty.resources.ConnectionProvider; -import reactor.netty.resources.LoopResources; - -public final class WebsocketGatewayClient implements GatewayClient { - - private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketGatewayClient.class); - - private static final String STREAM_ID = "sid"; - - @SuppressWarnings("rawtypes") - private static final AtomicReferenceFieldUpdater - websocketMonoUpdater = - AtomicReferenceFieldUpdater.newUpdater( - WebsocketGatewayClient.class, Mono.class, "websocketMono"); - - private final AtomicLong sidCounter = new AtomicLong(); - - private final GatewayClientCodec codec; - private final GatewayClientSettings settings; - private final HttpClient httpClient; - private final LoopResources loopResources; - private final boolean ownsLoopResources; - - private final Sinks.One close = Sinks.one(); - private final Sinks.One onClose = Sinks.one(); - - @SuppressWarnings("unused") - private volatile Mono websocketMono; - - /** - * Creates instance of websocket client transport. - * - * @param settings client settings - * @param codec client codec. - */ - public WebsocketGatewayClient(GatewayClientSettings settings, GatewayClientCodec codec) { - this(settings, codec, LoopResources.create("websocket-gateway-client"), true); - } - - /** - * Creates instance of websocket client transport. - * - * @param settings client settings - * @param codec client codec. - * @param loopResources loopResources. - */ - public WebsocketGatewayClient( - GatewayClientSettings settings, - GatewayClientCodec codec, - LoopResources loopResources) { - this(settings, codec, loopResources, false); - } - - private WebsocketGatewayClient( - GatewayClientSettings settings, - GatewayClientCodec codec, - LoopResources loopResources, - boolean ownsLoopResources) { - - this.settings = settings; - this.codec = codec; - this.loopResources = loopResources; - this.ownsLoopResources = ownsLoopResources; - - HttpClient httpClient = - HttpClient.create(ConnectionProvider.newConnection()) - .headers(headers -> settings.headers().forEach(headers::add)) - .followRedirect(settings.followRedirect()) - .wiretap(settings.wiretap()) - .runOn(loopResources) - .host(settings.host()) - .port(settings.port()); - - if (settings.sslProvider() != null) { - httpClient = httpClient.secure(settings.sslProvider()); - } - - this.httpClient = httpClient; - - // Setup cleanup - close - .asMono() - .then(doClose()) - .doFinally(s -> onClose.emitEmpty(busyLooping(Duration.ofSeconds(3)))) - .doOnTerminate(() -> LOGGER.info("Closed client")) - .subscribe(null, ex -> LOGGER.warn("Failed to close client, cause: " + ex)); - } - - @Override - public Mono requestResponse(ServiceMessage request) { - return getOrConnect() - .flatMap( - session -> { - long sid = sidCounter.incrementAndGet(); - return session - .send(encodeRequest(request, sid)) - .doOnSubscribe(s -> LOGGER.debug("Sending request {}", request)) - .then(session.newMonoProcessor(sid).asMono()) - .doOnCancel(() -> session.cancel(sid, request.qualifier())) - .doFinally(s -> session.removeProcessor(sid)); - }); - } - - @Override - public Flux requestStream(ServiceMessage request) { - return getOrConnect() - .flatMapMany( - session -> { - long sid = sidCounter.incrementAndGet(); - return session - .send(encodeRequest(request, sid)) - .doOnSubscribe(s -> LOGGER.debug("Sending request {}", request)) - .thenMany(session.newUnicastProcessor(sid).asFlux()) - .doOnCancel(() -> session.cancel(sid, request.qualifier())) - .doFinally(s -> session.removeProcessor(sid)); - }); - } - - @Override - public Flux requestChannel(Flux requests) { - return Flux.error(new UnsupportedOperationException("requestChannel is not supported")); - } - - @Override - public void close() { - close.emitEmpty(busyLooping(Duration.ofSeconds(3))); - } - - @Override - public Mono onClose() { - return onClose.asMono(); - } - - private Mono doClose() { - return ownsLoopResources ? Mono.defer(loopResources::disposeLater) : Mono.empty(); - } - - private Mono getOrConnect() { - // noinspection unchecked - return websocketMonoUpdater.updateAndGet(this, this::getOrConnect0); - } - - private Mono getOrConnect0( - Mono prev) { - if (prev != null) { - return prev; - } - - Duration keepAliveInterval = settings.keepAliveInterval(); - - return httpClient - .websocket() - .uri("/") - .connect() - .map( - connection -> - keepAliveInterval != Duration.ZERO - ? connection - .onReadIdle(keepAliveInterval.toMillis(), () -> onReadIdle(connection)) - .onWriteIdle(keepAliveInterval.toMillis(), () -> onWriteIdle(connection)) - : connection) - .map( - connection -> { - WebsocketGatewayClientSession session = - new WebsocketGatewayClientSession(codec, connection); - LOGGER.info("Created session: {}", session); - // setup shutdown hook - session - .onClose() - .doOnTerminate( - () -> { - websocketMonoUpdater.getAndSet(this, null); // clear reference - LOGGER.info("Closed session: {}", session); - }) - .subscribe( - null, - th -> - LOGGER.warn( - "Exception on closing session: {}, cause: {}", - session, - th.toString())); - return session; - }) - .doOnError( - ex -> { - LOGGER.warn( - "Failed to connect on {}:{}, cause: {}", settings.host(), settings.port(), ex); - websocketMonoUpdater.getAndSet(this, null); // clear reference - }) - .cache(); - } - - private void onWriteIdle(Connection connection) { - LOGGER.debug("Sending keepalive on writeIdle"); - connection - .outbound() - .sendObject(new PingWebSocketFrame()) - .then() - .subscribe(null, ex -> LOGGER.warn("Can't send keepalive on writeIdle: " + ex)); - } - - private void onReadIdle(Connection connection) { - LOGGER.debug("Sending keepalive on readIdle"); - connection - .outbound() - .sendObject(new PingWebSocketFrame()) - .then() - .subscribe(null, ex -> LOGGER.warn("Can't send keepalive on readIdle: " + ex)); - } - - private ByteBuf encodeRequest(ServiceMessage message, long sid) { - return codec.encode(ServiceMessage.from(message).header(STREAM_ID, sid).build()); - } -} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/GatewayMessages.java b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/GatewayMessages.java similarity index 98% rename from services-gateway/src/main/java/io/scalecube/services/gateway/ws/GatewayMessages.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/websocket/GatewayMessages.java index 0aef0b0e8..0c69a9a8f 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/GatewayMessages.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/GatewayMessages.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.ws; +package io.scalecube.services.gateway.websocket; import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.exceptions.ServiceProviderErrorMapper; diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/Signal.java b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/Signal.java similarity index 92% rename from services-gateway/src/main/java/io/scalecube/services/gateway/ws/Signal.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/websocket/Signal.java index 5584fd025..f202e609e 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/Signal.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/Signal.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.ws; +package io.scalecube.services.gateway.websocket; public enum Signal { COMPLETE(1), diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketContextException.java b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketContextException.java similarity index 95% rename from services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketContextException.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketContextException.java index 5f2928767..ffc4b8726 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketContextException.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketContextException.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.ws; +package io.scalecube.services.gateway.websocket; import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.gateway.ReferenceCountUtil; diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGateway.java b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGateway.java new file mode 100644 index 000000000..d3beb6ef8 --- /dev/null +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGateway.java @@ -0,0 +1,196 @@ +package io.scalecube.services.gateway.websocket; + +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.scalecube.services.Address; +import io.scalecube.services.exceptions.DefaultErrorMapper; +import io.scalecube.services.exceptions.ServiceProviderErrorMapper; +import io.scalecube.services.gateway.Gateway; +import io.scalecube.services.gateway.GatewayOptions; +import io.scalecube.services.gateway.GatewaySessionHandler; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.StringJoiner; +import java.util.function.UnaryOperator; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; +import reactor.netty.resources.LoopResources; + +public class WebsocketGateway implements Gateway { + + private final GatewayOptions options; + private final GatewaySessionHandler gatewayHandler; + private final Duration keepAliveInterval; + private final ServiceProviderErrorMapper errorMapper; + + private DisposableServer server; + private LoopResources loopResources; + + private WebsocketGateway(Builder builder) { + this.options = builder.options; + this.gatewayHandler = builder.gatewayHandler; + this.keepAliveInterval = builder.keepAliveInterval; + this.errorMapper = builder.errorMapper; + } + + public WebsocketGateway(UnaryOperator operator) { + this(operator.apply(new Builder())); + } + + @Override + public String id() { + return options.id(); + } + + @Override + public Mono start() { + return Mono.defer( + () -> { + WebsocketGatewayAcceptor gatewayAcceptor = + new WebsocketGatewayAcceptor(options.call(), gatewayHandler, errorMapper); + + loopResources = LoopResources.create(options.id() + ":" + options.port()); + + return prepareHttpServer(loopResources, options.port()) + .doOnConnection(this::setupKeepAlive) + .handle(gatewayAcceptor) + .bind() + .doOnSuccess(server -> this.server = server) + .thenReturn(this); + }); + } + + private HttpServer prepareHttpServer(LoopResources loopResources, int port) { + return HttpServer.create() + .tcpConfiguration( + tcpServer -> { + if (loopResources != null) { + tcpServer = tcpServer.runOn(loopResources); + } + return tcpServer.bindAddress(() -> new InetSocketAddress(port)); + }); + } + + @Override + public Address address() { + InetSocketAddress address = (InetSocketAddress) server.address(); + return Address.create(address.getHostString(), address.getPort()); + } + + @Override + public Mono stop() { + return Flux.concatDelayError(shutdownServer(server), shutdownLoopResources(loopResources)) + .then(); + } + + private Mono shutdownServer(DisposableServer server) { + return Mono.defer( + () -> { + if (server != null) { + server.dispose(); + return server.onDispose(); + } + return Mono.empty(); + }); + } + + private Mono shutdownLoopResources(LoopResources loopResources) { + return loopResources != null ? loopResources.disposeLater() : Mono.empty(); + } + + private void setupKeepAlive(Connection connection) { + if (keepAliveInterval != Duration.ZERO) { + connection + .onReadIdle(keepAliveInterval.toMillis(), () -> onReadIdle(connection)) + .onWriteIdle(keepAliveInterval.toMillis(), () -> onWriteIdle(connection)); + } + } + + private void onWriteIdle(Connection connection) { + connection + .outbound() + .sendObject(new PingWebSocketFrame()) + .then() + .subscribe( + null, + ex -> { + // no-op + }); + } + + private void onReadIdle(Connection connection) { + connection + .outbound() + .sendObject(new PingWebSocketFrame()) + .then() + .subscribe( + null, + ex -> { + // no-op + }); + } + + @Override + public String toString() { + return new StringJoiner(", ", WebsocketGateway.class.getSimpleName() + "[", "]") + .add("options=" + options) + .add("gatewayHandler=" + gatewayHandler) + .add("keepAliveInterval=" + keepAliveInterval) + .add("errorMapper=" + errorMapper) + .add("server=" + server) + .add("loopResources=" + loopResources) + .toString(); + } + + public static class Builder { + + private GatewayOptions options; + private GatewaySessionHandler gatewayHandler = GatewaySessionHandler.DEFAULT_INSTANCE; + private Duration keepAliveInterval = Duration.ZERO; + private ServiceProviderErrorMapper errorMapper = DefaultErrorMapper.INSTANCE; + + public Builder() {} + + public GatewayOptions options() { + return options; + } + + public Builder options(GatewayOptions options) { + this.options = options; + return this; + } + + public GatewaySessionHandler gatewayHandler() { + return gatewayHandler; + } + + public Builder gatewayHandler(GatewaySessionHandler gatewayHandler) { + this.gatewayHandler = gatewayHandler; + return this; + } + + public Duration keepAliveInterval() { + return keepAliveInterval; + } + + public Builder keepAliveInterval(Duration keepAliveInterval) { + this.keepAliveInterval = keepAliveInterval; + return this; + } + + public ServiceProviderErrorMapper errorMapper() { + return errorMapper; + } + + public Builder errorMapper(ServiceProviderErrorMapper errorMapper) { + this.errorMapper = errorMapper; + return this; + } + + public WebsocketGateway build() { + return new WebsocketGateway(this); + } + } +} diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGatewayAcceptor.java b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGatewayAcceptor.java similarity index 92% rename from services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGatewayAcceptor.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGatewayAcceptor.java index 910d0c8d0..8ae963908 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGatewayAcceptor.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGatewayAcceptor.java @@ -1,13 +1,13 @@ -package io.scalecube.services.gateway.ws; - -import static io.scalecube.services.gateway.ws.GatewayMessages.RATE_LIMIT_FIELD; -import static io.scalecube.services.gateway.ws.GatewayMessages.getSid; -import static io.scalecube.services.gateway.ws.GatewayMessages.getSignal; -import static io.scalecube.services.gateway.ws.GatewayMessages.newCancelMessage; -import static io.scalecube.services.gateway.ws.GatewayMessages.newCompleteMessage; -import static io.scalecube.services.gateway.ws.GatewayMessages.newResponseMessage; -import static io.scalecube.services.gateway.ws.GatewayMessages.toErrorResponse; -import static io.scalecube.services.gateway.ws.GatewayMessages.validateSidOnSession; +package io.scalecube.services.gateway.websocket; + +import static io.scalecube.services.gateway.websocket.GatewayMessages.RATE_LIMIT_FIELD; +import static io.scalecube.services.gateway.websocket.GatewayMessages.getSid; +import static io.scalecube.services.gateway.websocket.GatewayMessages.getSignal; +import static io.scalecube.services.gateway.websocket.GatewayMessages.newCancelMessage; +import static io.scalecube.services.gateway.websocket.GatewayMessages.newCompleteMessage; +import static io.scalecube.services.gateway.websocket.GatewayMessages.newResponseMessage; +import static io.scalecube.services.gateway.websocket.GatewayMessages.toErrorResponse; +import static io.scalecube.services.gateway.websocket.GatewayMessages.validateSidOnSession; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGatewaySession.java b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGatewaySession.java similarity index 93% rename from services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGatewaySession.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGatewaySession.java index 33967ebf0..1d97d9db1 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGatewaySession.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketGatewaySession.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.ws; +package io.scalecube.services.gateway.websocket; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; @@ -201,20 +201,14 @@ public void register(Long streamId, Disposable disposable) { } if (result) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Registered subscription with sid={}, session={}", streamId, sessionId); + LOGGER.debug("Registered subscription by sid={}, session={}", streamId, sessionId); } } } private void clearSubscriptions() { - if (subscriptions.size() > 1) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Clear all {} subscriptions on session={}", subscriptions.size(), sessionId); - } - } else if (subscriptions.size() == 1) { - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Clear 1 subscription on session={}", sessionId); - } + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Clear subscriptions on session={}", sessionId); } subscriptions.forEach((sid, disposable) -> disposable.dispose()); subscriptions.clear(); diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketServiceMessageCodec.java b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketServiceMessageCodec.java similarity index 95% rename from services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketServiceMessageCodec.java rename to services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketServiceMessageCodec.java index 0ebbbcdbb..4699f3c1b 100644 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketServiceMessageCodec.java +++ b/services-gateway/src/main/java/io/scalecube/services/gateway/websocket/WebsocketServiceMessageCodec.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.ws; +package io.scalecube.services.gateway.websocket; import static com.fasterxml.jackson.core.JsonToken.VALUE_NULL; @@ -24,13 +24,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Map.Entry; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public final class WebsocketServiceMessageCodec { - private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketServiceMessageCodec.class); - private static final ObjectMapper objectMapper = objectMapper(); private static final MappingJsonFactory jsonFactory = new MappingJsonFactory(objectMapper); @@ -102,7 +98,6 @@ public ByteBuf encode(ServiceMessage message) throws MessageCodecException { if (message.data() != null) { ReferenceCountUtil.safestRelease(message.data()); } - LOGGER.error("Failed to encode gateway service message: {}", message, ex); throw new MessageCodecException("Failed to encode gateway service message", ex); } return byteBuf; diff --git a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGateway.java b/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGateway.java deleted file mode 100644 index 25e465702..000000000 --- a/services-gateway/src/main/java/io/scalecube/services/gateway/ws/WebsocketGateway.java +++ /dev/null @@ -1,163 +0,0 @@ -package io.scalecube.services.gateway.ws; - -import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; -import io.scalecube.services.Address; -import io.scalecube.services.exceptions.DefaultErrorMapper; -import io.scalecube.services.exceptions.ServiceProviderErrorMapper; -import io.scalecube.services.gateway.Gateway; -import io.scalecube.services.gateway.GatewayOptions; -import io.scalecube.services.gateway.GatewaySessionHandler; -import io.scalecube.services.gateway.GatewayTemplate; -import java.net.InetSocketAddress; -import java.time.Duration; -import java.util.StringJoiner; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.netty.Connection; -import reactor.netty.DisposableServer; -import reactor.netty.resources.LoopResources; - -public class WebsocketGateway extends GatewayTemplate { - - private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketGateway.class); - - private final GatewaySessionHandler gatewayHandler; - private final Duration keepAliveInterval; - private final ServiceProviderErrorMapper errorMapper; - - private DisposableServer server; - private LoopResources loopResources; - - /** - * Constructor. - * - * @param options gateway options - */ - public WebsocketGateway(GatewayOptions options) { - this( - options, - Duration.ZERO, - GatewaySessionHandler.DEFAULT_INSTANCE, - DefaultErrorMapper.INSTANCE); - } - - /** - * Constructor. - * - * @param options gateway options - * @param keepAliveInterval keep alive interval - */ - public WebsocketGateway(GatewayOptions options, Duration keepAliveInterval) { - this( - options, - keepAliveInterval, - GatewaySessionHandler.DEFAULT_INSTANCE, - DefaultErrorMapper.INSTANCE); - } - - /** - * Constructor. - * - * @param options gateway options - * @param gatewayHandler gateway handler - */ - public WebsocketGateway(GatewayOptions options, GatewaySessionHandler gatewayHandler) { - this(options, Duration.ZERO, gatewayHandler, DefaultErrorMapper.INSTANCE); - } - - /** - * Constructor. - * - * @param options gateway options - * @param errorMapper error mapper - */ - public WebsocketGateway(GatewayOptions options, ServiceProviderErrorMapper errorMapper) { - this(options, Duration.ZERO, GatewaySessionHandler.DEFAULT_INSTANCE, errorMapper); - } - - /** - * Constructor. - * - * @param options gateway options - * @param keepAliveInterval keep alive interval - * @param gatewayHandler gateway handler - * @param errorMapper error mapper - */ - public WebsocketGateway( - GatewayOptions options, - Duration keepAliveInterval, - GatewaySessionHandler gatewayHandler, - ServiceProviderErrorMapper errorMapper) { - super(options); - this.keepAliveInterval = keepAliveInterval; - this.gatewayHandler = gatewayHandler; - this.errorMapper = errorMapper; - } - - @Override - public Mono start() { - return Mono.defer( - () -> { - WebsocketGatewayAcceptor acceptor = - new WebsocketGatewayAcceptor(options.call(), gatewayHandler, errorMapper); - - loopResources = LoopResources.create("websocket-gateway"); - - return prepareHttpServer(loopResources, options.port()) - .doOnConnection(this::setupKeepAlive) - .handle(acceptor) - .bind() - .doOnSuccess(server -> this.server = server) - .thenReturn(this); - }); - } - - @Override - public Address address() { - InetSocketAddress address = (InetSocketAddress) server.address(); - return Address.create(address.getHostString(), address.getPort()); - } - - @Override - public Mono stop() { - return Flux.concatDelayError(shutdownServer(server), shutdownLoopResources(loopResources)) - .then(); - } - - @Override - public String toString() { - return new StringJoiner(", ", WebsocketGateway.class.getSimpleName() + "[", "]") - .add("server=" + server) - .add("loopResources=" + loopResources) - .add("options=" + options) - .toString(); - } - - private void setupKeepAlive(Connection connection) { - if (keepAliveInterval != Duration.ZERO) { - connection - .onReadIdle(keepAliveInterval.toMillis(), () -> onReadIdle(connection)) - .onWriteIdle(keepAliveInterval.toMillis(), () -> onWriteIdle(connection)); - } - } - - private void onWriteIdle(Connection connection) { - LOGGER.debug("Sending keepalive on writeIdle"); - connection - .outbound() - .sendObject(new PingWebSocketFrame()) - .then() - .subscribe(null, ex -> LOGGER.warn("Can't send keepalive on writeIdle: " + ex)); - } - - private void onReadIdle(Connection connection) { - LOGGER.debug("Sending keepalive on readIdle"); - connection - .outbound() - .sendObject(new PingWebSocketFrame()) - .then() - .subscribe(null, ex -> LOGGER.warn("Can't send keepalive on readIdle: " + ex)); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/AbstractGatewayExtension.java b/services-gateway/src/test/java/io/scalecube/services/gateway/AbstractGatewayExtension.java deleted file mode 100644 index 7d5e3f1ac..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/AbstractGatewayExtension.java +++ /dev/null @@ -1,139 +0,0 @@ -package io.scalecube.services.gateway; - -import io.scalecube.services.Address; -import io.scalecube.services.Microservices; -import io.scalecube.services.ServiceCall; -import io.scalecube.services.ServiceEndpoint; -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.discovery.ScalecubeServiceDiscovery; -import io.scalecube.services.discovery.api.ServiceDiscovery; -import io.scalecube.services.gateway.transport.GatewayClientSettings; -import io.scalecube.services.gateway.transport.StaticAddressRouter; -import io.scalecube.services.transport.api.ClientTransport; -import io.scalecube.services.transport.rsocket.RSocketServiceTransport; -import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; -import java.util.function.Function; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterEachCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class AbstractGatewayExtension - implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { - - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractGatewayExtension.class); - - private final ServiceInfo serviceInfo; - private final Function gatewaySupplier; - private final Function clientSupplier; - - private String gatewayId; - private Microservices gateway; - private Microservices services; - private ServiceCall clientServiceCall; - - protected AbstractGatewayExtension( - ServiceInfo serviceInfo, - Function gatewaySupplier, - Function clientSupplier) { - this.serviceInfo = serviceInfo; - this.gatewaySupplier = gatewaySupplier; - this.clientSupplier = clientSupplier; - } - - @Override - public final void beforeAll(ExtensionContext context) { - gateway = - Microservices.builder() - .discovery( - serviceEndpoint -> - new ScalecubeServiceDiscovery() - .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) - .options(opts -> opts.metadata(serviceEndpoint))) - .transport(RSocketServiceTransport::new) - .gateway( - options -> { - Gateway gateway = gatewaySupplier.apply(options); - gatewayId = gateway.id(); - return gateway; - }) - .startAwait(); - startServices(); - } - - @Override - public final void beforeEach(ExtensionContext context) { - // if services was shutdown in test need to start them again - if (services == null) { - startServices(); - } - Address gatewayAddress = gateway.gateway(gatewayId).address(); - GatewayClientSettings clintSettings = - GatewayClientSettings.builder().address(gatewayAddress).build(); - clientServiceCall = - new ServiceCall() - .transport(clientSupplier.apply(clintSettings)) - .router(new StaticAddressRouter(gatewayAddress)); - } - - @Override - public final void afterEach(ExtensionContext context) { - // no-op - } - - @Override - public final void afterAll(ExtensionContext context) { - shutdownServices(); - shutdownGateway(); - } - - public ServiceCall client() { - return clientServiceCall; - } - - public void startServices() { - services = - Microservices.builder() - .discovery(this::serviceDiscovery) - .transport(RSocketServiceTransport::new) - .services(serviceInfo) - .startAwait(); - LOGGER.info("Started services {} on {}", services, services.serviceAddress()); - } - - private ServiceDiscovery serviceDiscovery(ServiceEndpoint serviceEndpoint) { - return new ScalecubeServiceDiscovery() - .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) - .options(opts -> opts.metadata(serviceEndpoint)) - .membership(opts -> opts.seedMembers(gateway.discoveryAddress().toString())); - } - - public void shutdownServices() { - if (services != null) { - try { - services.shutdown().block(); - } catch (Throwable ignore) { - // ignore - } - LOGGER.info("Shutdown services {}", services); - - // if this method is called in particular test need to indicate that services are stopped to - // start them again before another test - services = null; - } - } - - private void shutdownGateway() { - if (gateway != null) { - try { - gateway.shutdown().block(); - } catch (Throwable ignore) { - // ignore - } - LOGGER.info("Shutdown gateway {}", gateway); - } - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/AbstractLocalGatewayExtension.java b/services-gateway/src/test/java/io/scalecube/services/gateway/AbstractLocalGatewayExtension.java deleted file mode 100644 index a09d3fbbb..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/AbstractLocalGatewayExtension.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.scalecube.services.gateway; - -import io.scalecube.services.Address; -import io.scalecube.services.Microservices; -import io.scalecube.services.ServiceCall; -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.transport.GatewayClientSettings; -import io.scalecube.services.gateway.transport.StaticAddressRouter; -import io.scalecube.services.transport.api.ClientTransport; -import java.util.Optional; -import java.util.function.Function; -import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.BeforeAllCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.netty.resources.LoopResources; - -public abstract class AbstractLocalGatewayExtension - implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback { - - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLocalGatewayExtension.class); - - private final ServiceInfo serviceInfo; - private final Function gatewaySupplier; - private final Function clientSupplier; - - private Microservices gateway; - private LoopResources clientLoopResources; - private ServiceCall clientServiceCall; - private String gatewayId; - - protected AbstractLocalGatewayExtension( - ServiceInfo serviceInfo, - Function gatewaySupplier, - Function clientSupplier) { - this.serviceInfo = serviceInfo; - this.gatewaySupplier = gatewaySupplier; - this.clientSupplier = clientSupplier; - } - - @Override - public final void beforeAll(ExtensionContext context) { - - gateway = - Microservices.builder() - .services(serviceInfo) - .gateway( - options -> { - Gateway gateway = gatewaySupplier.apply(options); - gatewayId = gateway.id(); - return gateway; - }) - .startAwait(); - - clientLoopResources = LoopResources.create("gateway-client-transport-worker"); - } - - @Override - public final void beforeEach(ExtensionContext context) { - Address address = gateway.gateway(gatewayId).address(); - - GatewayClientSettings settings = GatewayClientSettings.builder().address(address).build(); - - clientServiceCall = - new ServiceCall() - .transport(clientSupplier.apply(settings)) - .router(new StaticAddressRouter(address)); - } - - @Override - public final void afterAll(ExtensionContext context) { - Optional.ofNullable(clientLoopResources).ifPresent(LoopResources::dispose); - shutdownGateway(); - } - - public ServiceCall client() { - return clientServiceCall; - } - - private void shutdownGateway() { - if (gateway != null) { - try { - gateway.shutdown().block(); - } catch (Throwable ignore) { - // ignore - } - LOGGER.info("Shutdown gateway {}", gateway); - } - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/ErrorService.java b/services-gateway/src/test/java/io/scalecube/services/gateway/ErrorService.java similarity index 86% rename from services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/ErrorService.java rename to services-gateway/src/test/java/io/scalecube/services/gateway/ErrorService.java index 2ad2d183c..f00a269aa 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/ErrorService.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/ErrorService.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.exceptions; +package io.scalecube.services.gateway; import io.scalecube.services.annotations.Service; import io.scalecube.services.annotations.ServiceMethod; diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/ErrorServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/ErrorServiceImpl.java similarity index 86% rename from services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/ErrorServiceImpl.java rename to services-gateway/src/test/java/io/scalecube/services/gateway/ErrorServiceImpl.java index ba5042ea5..c50927d51 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/ErrorServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/ErrorServiceImpl.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.exceptions; +package io.scalecube.services.gateway; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/GatewayErrorMapperImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/GatewayErrorMapperImpl.java similarity index 96% rename from services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/GatewayErrorMapperImpl.java rename to services-gateway/src/test/java/io/scalecube/services/gateway/GatewayErrorMapperImpl.java index 792a1e457..317f00707 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/GatewayErrorMapperImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/GatewayErrorMapperImpl.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.exceptions; +package io.scalecube.services.gateway; import io.scalecube.services.api.ErrorData; import io.scalecube.services.api.ServiceMessage; diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/SecuredService.java b/services-gateway/src/test/java/io/scalecube/services/gateway/SecuredService.java index 8a82038d4..28313dd9f 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/SecuredService.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/SecuredService.java @@ -1,6 +1,6 @@ package io.scalecube.services.gateway; -import static io.scalecube.services.gateway.SecuredService.NS; +import static io.scalecube.services.gateway.SecuredService.NAMESPACE; import io.scalecube.services.annotations.RequestType; import io.scalecube.services.annotations.Service; @@ -10,10 +10,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -/** Authentication service and the service body itself in one class. */ -@Service(NS) +@Service(NAMESPACE) public interface SecuredService { - String NS = "gw.auth"; + + String NAMESPACE = "gw.auth"; @ServiceMethod @RequestType(String.class) diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/SecuredServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/SecuredServiceImpl.java index 89059a948..021ef67fa 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/SecuredServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/SecuredServiceImpl.java @@ -12,6 +12,7 @@ import reactor.core.publisher.Mono; public class SecuredServiceImpl implements SecuredService { + private static final Logger LOGGER = LoggerFactory.getLogger(SecuredServiceImpl.class); private static final String ALLOWED_USER = "VASYA_PUPKIN"; diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/SomeException.java b/services-gateway/src/test/java/io/scalecube/services/gateway/SomeException.java similarity index 87% rename from services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/SomeException.java rename to services-gateway/src/test/java/io/scalecube/services/gateway/SomeException.java index 49bc79431..0f2af4a96 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/exceptions/SomeException.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/SomeException.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.exceptions; +package io.scalecube.services.gateway; import io.scalecube.services.exceptions.ServiceException; diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/TestServiceImpl.java b/services-gateway/src/test/java/io/scalecube/services/gateway/TestServiceImpl.java index 1983b16be..7a8480f1e 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/TestServiceImpl.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/TestServiceImpl.java @@ -14,7 +14,7 @@ public TestServiceImpl(Runnable onClose) { @Override public Flux manyNever() { - return Flux.never().log(">>> ").doOnCancel(onClose); + return Flux.never().log(">>>").doOnCancel(onClose); } @Override diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/TestUtils.java b/services-gateway/src/test/java/io/scalecube/services/gateway/TestUtils.java deleted file mode 100644 index 0742fc4d8..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/TestUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.scalecube.services.gateway; - -import java.time.Duration; -import java.util.function.BooleanSupplier; -import reactor.core.publisher.Mono; - -public final class TestUtils { - - public static final Duration TIMEOUT = Duration.ofSeconds(10); - - private TestUtils() {} - - /** - * Waits until the given condition is done - * - * @param condition condition - * @return operation's result - */ - public static Mono await(BooleanSupplier condition) { - return Mono.delay(Duration.ofMillis(100)).repeat(() -> !condition.getAsBoolean()).then(); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/http/CorsTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/http/CorsTest.java index d7b2ca0b0..b22da4be8 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/http/CorsTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/http/CorsTest.java @@ -7,13 +7,11 @@ import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponseStatus; +import io.scalecube.services.Address; import io.scalecube.services.Microservices; -import io.scalecube.services.discovery.ScalecubeServiceDiscovery; import io.scalecube.services.examples.GreetingService; import io.scalecube.services.examples.GreetingServiceImpl; import io.scalecube.services.gateway.BaseTest; -import io.scalecube.services.transport.rsocket.RSocketServiceTransport; -import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; import java.time.Duration; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -27,24 +25,13 @@ public class CorsTest extends BaseTest { private static final Duration TIMEOUT = Duration.ofSeconds(3); - public static final int HTTP_PORT = 8999; private Microservices gateway; - - private final Microservices.Builder gatewayBuilder = - Microservices.builder() - .discovery( - serviceEndpoint -> - new ScalecubeServiceDiscovery() - .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) - .options(opts -> opts.metadata(serviceEndpoint))) - .transport(RSocketServiceTransport::new) - .services(new GreetingServiceImpl()); - private HttpClient client; + private ConnectionProvider connectionProvider; @BeforeEach - void beforeEach() { - client = HttpClient.create(ConnectionProvider.newConnection()).port(HTTP_PORT).wiretap(true); + void setUp() { + connectionProvider = ConnectionProvider.newConnection(); } @AfterEach @@ -52,22 +39,30 @@ void afterEach() { if (gateway != null) { gateway.shutdown().block(); } + if (connectionProvider != null) { + connectionProvider.dispose(); + } } @Test void testCrossOriginRequest() { gateway = - gatewayBuilder + Microservices.builder() .gateway( opts -> - new HttpGateway(opts.id("http").port(HTTP_PORT)) + new HttpGateway.Builder() + .options(opts.id("http")) .corsEnabled(true) - .corsConfig( - config -> - config.allowedRequestHeaders("Content-Type", "X-Correlation-ID"))) + .corsConfigBuilder( + builder -> + builder.allowedRequestHeaders("Content-Type", "X-Correlation-ID")) + .build()) + .services(new GreetingServiceImpl()) .start() .block(TIMEOUT); + final HttpClient client = newClient(gateway.gateway("http").address()); + HttpClientResponse response = client .headers( @@ -109,14 +104,21 @@ void testCrossOriginRequest() { assertEquals("*", responseHeaders.get("Access-Control-Allow-Origin")); } + private HttpClient newClient(final Address address) { + return HttpClient.create(connectionProvider).port(address.port()); + } + @Test void testOptionRequestCorsDisabled() { gateway = - gatewayBuilder - .gateway(opts -> new HttpGateway(opts.id("http").port(HTTP_PORT)).corsEnabled(false)) + Microservices.builder() + .gateway(opts -> new HttpGateway.Builder().options(opts.id("http")).build()) + .services(new GreetingServiceImpl()) .start() .block(TIMEOUT); + final HttpClient client = newClient(gateway.gateway("http").address()); + HttpClientResponse response = client .headers( diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpClientConnectionTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpClientConnectionTest.java index f100ce12b..77a5ae0fd 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpClientConnectionTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpClientConnectionTest.java @@ -1,6 +1,7 @@ package io.scalecube.services.gateway.http; -import io.netty.buffer.ByteBuf; +import static org.junit.jupiter.api.Assertions.assertEquals; + import io.scalecube.services.Address; import io.scalecube.services.Microservices; import io.scalecube.services.ServiceCall; @@ -8,13 +9,8 @@ import io.scalecube.services.annotations.ServiceMethod; import io.scalecube.services.discovery.ScalecubeServiceDiscovery; import io.scalecube.services.gateway.BaseTest; -import io.scalecube.services.gateway.transport.GatewayClient; -import io.scalecube.services.gateway.transport.GatewayClientCodec; -import io.scalecube.services.gateway.transport.GatewayClientSettings; -import io.scalecube.services.gateway.transport.GatewayClientTransport; -import io.scalecube.services.gateway.transport.GatewayClientTransports; -import io.scalecube.services.gateway.transport.StaticAddressRouter; -import io.scalecube.services.gateway.transport.http.HttpGatewayClient; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.http.HttpGatewayClientTransport; import io.scalecube.services.transport.rsocket.RSocketServiceTransport; import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; import java.io.IOException; @@ -29,15 +25,13 @@ class HttpClientConnectionTest extends BaseTest { - public static final GatewayClientCodec CLIENT_CODEC = - GatewayClientTransports.HTTP_CLIENT_CODEC; + private static final Duration TIMEOUT = Duration.ofSeconds(10); private Microservices gateway; private Address gatewayAddress; - private Microservices service; + private Microservices microservices; - private static final AtomicInteger onCloseCounter = new AtomicInteger(); - private GatewayClient client; + private final AtomicInteger onCancelCounter = new AtomicInteger(); @BeforeEach void beforEach() { @@ -49,12 +43,12 @@ void beforEach() { .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) .options(opts -> opts.metadata(serviceEndpoint))) .transport(RSocketServiceTransport::new) - .gateway(options -> new HttpGateway(options.id("HTTP"))) + .gateway(options -> new HttpGateway.Builder().options(options.id("HTTP")).build()) .startAwait(); gatewayAddress = gateway.gateway("HTTP").address(); - service = + microservices = Microservices.builder() .discovery( serviceEndpoint -> @@ -64,74 +58,74 @@ void beforEach() { .membership( opts -> opts.seedMembers(gateway.discoveryAddress().toString()))) .transport(RSocketServiceTransport::new) - .services(new TestServiceImpl()) + .services(new TestServiceImpl(onCancelCounter)) .startAwait(); - - onCloseCounter.set(0); } @AfterEach void afterEach() { Flux.concat( - Mono.justOrEmpty(client).doOnNext(GatewayClient::close).flatMap(GatewayClient::onClose), Mono.justOrEmpty(gateway).map(Microservices::shutdown), - Mono.justOrEmpty(service).map(Microservices::shutdown)) + Mono.justOrEmpty(microservices).map(Microservices::shutdown)) .then() .block(); } @Test void testCloseServiceStreamAfterLostConnection() { - client = - new HttpGatewayClient( - GatewayClientSettings.builder().address(gatewayAddress).build(), CLIENT_CODEC); - - ServiceCall serviceCall = - new ServiceCall() - .transport(new GatewayClientTransport(client)) - .router(new StaticAddressRouter(gatewayAddress)); - - StepVerifier.create(serviceCall.api(TestService.class).oneNever("body").log("<<< ")) - .thenAwait(Duration.ofSeconds(5)) - .then(() -> client.close()) - .then(() -> client.onClose().block()) - .expectError(IOException.class) - .verify(Duration.ofSeconds(1)); + try (ServiceCall serviceCall = serviceCall(gatewayAddress)) { + StepVerifier.create(serviceCall.api(TestService.class).oneNever("body").log("<<<")) + .thenAwait(Duration.ofSeconds(3)) + .then(serviceCall::close) + .expectError(IOException.class) + .verify(TIMEOUT); + + Mono.delay(Duration.ofMillis(100)) + .repeat(() -> onCancelCounter.get() != 1) + .then() + .block(TIMEOUT); + + assertEquals(1, onCancelCounter.get()); + } } @Test public void testCallRepeatedlyByInvalidAddress() { - Address invalidAddress = Address.create("localhost", 5050); - - client = - new HttpGatewayClient( - GatewayClientSettings.builder().address(invalidAddress).build(), CLIENT_CODEC); - - ServiceCall serviceCall = - new ServiceCall() - .transport(new GatewayClientTransport(client)) - .router(new StaticAddressRouter(invalidAddress)); - - for (int i = 0; i < 100; i++) { - StepVerifier.create(serviceCall.api(TestService.class).oneNever("body").log("<<< ")) - .thenAwait(Duration.ofSeconds(1)) - .expectError(IOException.class) - .verify(Duration.ofSeconds(10)); + final Address address = Address.create("localhost", 5050); + try (ServiceCall serviceCall = serviceCall(address)) { + for (int i = 0; i < 15; i++) { + StepVerifier.create(serviceCall.api(TestService.class).oneNever("body").log("<<<")) + .thenAwait(Duration.ofSeconds(1)) + .expectError(IOException.class) + .verify(TIMEOUT); + } } } + private static ServiceCall serviceCall(Address address) { + return new ServiceCall() + .transport(new HttpGatewayClientTransport.Builder().address(address).build()) + .router(new StaticAddressRouter(address)); + } + @Service public interface TestService { - @ServiceMethod("oneNever") - Mono oneNever(String name); + @ServiceMethod + Mono oneNever(String name); } private static class TestServiceImpl implements TestService { + private final AtomicInteger onCancelCounter; + + public TestServiceImpl(AtomicInteger onCancelCounter) { + this.onCancelCounter = onCancelCounter; + } + @Override - public Mono oneNever(String name) { - return Mono.never().log(">>> ").doOnCancel(onCloseCounter::incrementAndGet); + public Mono oneNever(String name) { + return Mono.never().log(">>>").doOnCancel(onCancelCounter::incrementAndGet).then(); } } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpClientErrorMapperTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpClientErrorMapperTest.java deleted file mode 100644 index 88299caf9..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpClientErrorMapperTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.scalecube.services.gateway.http; - -import static io.scalecube.services.gateway.TestUtils.TIMEOUT; -import static io.scalecube.services.gateway.exceptions.GatewayErrorMapperImpl.ERROR_MAPPER; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.BaseTest; -import io.scalecube.services.gateway.exceptions.ErrorService; -import io.scalecube.services.gateway.exceptions.ErrorServiceImpl; -import io.scalecube.services.gateway.exceptions.SomeException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import reactor.test.StepVerifier; - -@Disabled("Cannot deserialize instance of `java.lang.String` out of START_OBJECT token") -class HttpClientErrorMapperTest extends BaseTest { - - @RegisterExtension - static HttpGatewayExtension extension = - new HttpGatewayExtension( - ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) - .errorMapper(ERROR_MAPPER) - .build()); - - private ErrorService service; - - @BeforeEach - void initService() { - service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); - } - - @Test - void shouldReturnSomeExceptionOnMono() { - StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpGatewayExtension.java b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpGatewayExtension.java deleted file mode 100644 index cb0c4de5a..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpGatewayExtension.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.scalecube.services.gateway.http; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.AbstractGatewayExtension; -import io.scalecube.services.gateway.transport.GatewayClientTransports; - -class HttpGatewayExtension extends AbstractGatewayExtension { - - private static final String GATEWAY_ALIAS_NAME = "http"; - - HttpGatewayExtension(Object serviceInstance) { - this(ServiceInfo.fromServiceInstance(serviceInstance).build()); - } - - HttpGatewayExtension(ServiceInfo serviceInfo) { - super( - serviceInfo, - opts -> new HttpGateway(opts.id(GATEWAY_ALIAS_NAME)), - GatewayClientTransports::httpGatewayClientTransport); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpGatewayTest.java index 4ece45976..c0d90ef15 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpGatewayTest.java @@ -1,42 +1,120 @@ package io.scalecube.services.gateway.http; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; +import static io.scalecube.services.gateway.GatewayErrorMapperImpl.ERROR_MAPPER; import static org.junit.jupiter.api.Assertions.assertEquals; +import io.scalecube.services.Address; +import io.scalecube.services.Microservices; +import io.scalecube.services.ServiceCall; +import io.scalecube.services.ServiceInfo; import io.scalecube.services.api.Qualifier; import io.scalecube.services.api.ServiceMessage; +import io.scalecube.services.discovery.ScalecubeServiceDiscovery; import io.scalecube.services.examples.EmptyGreetingRequest; import io.scalecube.services.examples.EmptyGreetingResponse; import io.scalecube.services.examples.GreetingRequest; import io.scalecube.services.examples.GreetingService; import io.scalecube.services.examples.GreetingServiceImpl; import io.scalecube.services.exceptions.InternalServiceException; -import io.scalecube.services.exceptions.ServiceUnavailableException; import io.scalecube.services.gateway.BaseTest; +import io.scalecube.services.gateway.ErrorService; +import io.scalecube.services.gateway.ErrorServiceImpl; +import io.scalecube.services.gateway.SomeException; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.http.HttpGatewayClientTransport; +import io.scalecube.services.transport.rsocket.RSocketServiceTransport; +import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; import reactor.test.StepVerifier; class HttpGatewayTest extends BaseTest { private static final Duration TIMEOUT = Duration.ofSeconds(3); - @RegisterExtension - static HttpGatewayExtension extension = new HttpGatewayExtension(new GreetingServiceImpl()); - - private GreetingService service; + private static Microservices gateway; + private static Address gatewayAddress; + private static StaticAddressRouter router; + private static Microservices microservices; + + private ServiceCall serviceCall; + private GreetingService greetingService; + private ErrorService errorService; + + @BeforeAll + static void beforeAll() { + gateway = + Microservices.builder() + .discovery( + serviceEndpoint -> + new ScalecubeServiceDiscovery() + .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) + .options(opts -> opts.metadata(serviceEndpoint))) + .transport(RSocketServiceTransport::new) + .gateway( + options -> + new HttpGateway.Builder() + .options(options.id("HTTP")) + .errorMapper(ERROR_MAPPER) + .build()) + .startAwait(); + + gatewayAddress = gateway.gateway("HTTP").address(); + router = new StaticAddressRouter(gatewayAddress); + + microservices = + Microservices.builder() + .discovery( + serviceEndpoint -> + new ScalecubeServiceDiscovery() + .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) + .options(opts -> opts.metadata(serviceEndpoint)) + .membership( + opts -> opts.seedMembers(gateway.discoveryAddress().toString()))) + .transport(RSocketServiceTransport::new) + .services(new GreetingServiceImpl()) + .services( + ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) + .errorMapper(ERROR_MAPPER) + .build()) + .startAwait(); + } @BeforeEach - void initService() { - service = extension.client().api(GreetingService.class); + void beforeEach() { + serviceCall = + new ServiceCall() + .router(router) + .transport(new HttpGatewayClientTransport.Builder().address(gatewayAddress).build()); + greetingService = serviceCall.api(GreetingService.class); + errorService = serviceCall.errorMapper(ERROR_MAPPER).api(ErrorService.class); + } + + @AfterEach + void afterEach() { + if (serviceCall != null) { + serviceCall.close(); + } + } + + @AfterAll + static void afterAll() { + if (gateway != null) { + gateway.close(); + } + if (microservices != null) { + microservices.close(); + } } @Test void shouldReturnSingleResponseWithSimpleRequest() { - StepVerifier.create(service.one("hello")) + StepVerifier.create(greetingService.one("hello")) .expectNext("Echo:hello") .expectComplete() .verify(TIMEOUT); @@ -45,7 +123,7 @@ void shouldReturnSingleResponseWithSimpleRequest() { @Test void shouldReturnSingleResponseWithSimpleLongDataRequest() { String data = new String(new char[500]); - StepVerifier.create(service.one(data)) + StepVerifier.create(greetingService.one(data)) .expectNext("Echo:" + data) .expectComplete() .verify(TIMEOUT); @@ -53,7 +131,7 @@ void shouldReturnSingleResponseWithSimpleLongDataRequest() { @Test void shouldReturnSingleResponseWithPojoRequest() { - StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) + StepVerifier.create(greetingService.pojoOne(new GreetingRequest("hello"))) .expectNextMatches(response -> "Echo:hello".equals(response.getText())) .expectComplete() .verify(TIMEOUT); @@ -61,7 +139,7 @@ void shouldReturnSingleResponseWithPojoRequest() { @Test void shouldReturnListResponseWithPojoRequest() { - StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) + StepVerifier.create(greetingService.pojoList(new GreetingRequest("hello"))) .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) .expectComplete() .verify(TIMEOUT); @@ -69,26 +147,12 @@ void shouldReturnListResponseWithPojoRequest() { @Test void shouldReturnNoContentWhenResponseIsEmpty() { - StepVerifier.create(service.emptyOne("hello")).expectComplete().verify(TIMEOUT); - } - - @Test - void shouldReturnServiceUnavailableWhenServiceIsDown() { - extension.shutdownServices(); - - StepVerifier.create(service.one("hello")) - .expectErrorSatisfies( - throwable -> { - assertEquals(ServiceUnavailableException.class, throwable.getClass()); - assertThat( - throwable.getMessage(), startsWith("No reachable member with such service:")); - }) - .verify(TIMEOUT); + StepVerifier.create(greetingService.emptyOne("hello")).expectComplete().verify(TIMEOUT); } @Test void shouldReturnInternalServerErrorWhenServiceFails() { - StepVerifier.create(service.failingOne("hello")) + StepVerifier.create(greetingService.failingOne("hello")) .expectErrorSatisfies( throwable -> { assertEquals(InternalServiceException.class, throwable.getClass()); @@ -99,12 +163,12 @@ void shouldReturnInternalServerErrorWhenServiceFails() { @Test void shouldSuccessfullyReuseServiceProxy() { - StepVerifier.create(service.one("hello")) + StepVerifier.create(greetingService.one("hello")) .expectNext("Echo:hello") .expectComplete() .verify(TIMEOUT); - StepVerifier.create(service.one("hello")) + StepVerifier.create(greetingService.one("hello")) .expectNext("Echo:hello") .expectComplete() .verify(TIMEOUT); @@ -112,7 +176,7 @@ void shouldSuccessfullyReuseServiceProxy() { @Test void shouldReturnNoEventOnNeverService() { - StepVerifier.create(service.neverOne("hi")) + StepVerifier.create(greetingService.neverOne("hi")) .expectSubscription() .expectNoEvent(Duration.ofSeconds(1)) .thenCancel() @@ -121,7 +185,7 @@ void shouldReturnNoEventOnNeverService() { @Test void shouldReturnOnEmptyGreeting() { - StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) + StepVerifier.create(greetingService.emptyGreeting(new EmptyGreetingRequest())) .expectSubscription() .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) .thenCancel() @@ -133,10 +197,16 @@ void shouldReturnOnEmptyMessageGreeting() { String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); ServiceMessage request = ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); - StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) + StepVerifier.create(serviceCall.requestOne(request, EmptyGreetingResponse.class)) .expectSubscription() .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) .thenCancel() .verify(); } + + @Disabled + @Test + void shouldReturnSomeException() { + StepVerifier.create(errorService.oneError()).expectError(SomeException.class).verify(TIMEOUT); + } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayErrorMapperTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayErrorMapperTest.java deleted file mode 100644 index 068631242..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayErrorMapperTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.scalecube.services.gateway.http; - -import static io.scalecube.services.gateway.TestUtils.TIMEOUT; -import static io.scalecube.services.gateway.exceptions.GatewayErrorMapperImpl.ERROR_MAPPER; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.BaseTest; -import io.scalecube.services.gateway.exceptions.ErrorService; -import io.scalecube.services.gateway.exceptions.ErrorServiceImpl; -import io.scalecube.services.gateway.exceptions.SomeException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import reactor.test.StepVerifier; - -@Disabled("Cannot deserialize instance of `java.lang.String` out of START_OBJECT token") -class HttpLocalGatewayErrorMapperTest extends BaseTest { - - @RegisterExtension - static HttpLocalGatewayExtension extension = - new HttpLocalGatewayExtension( - ServiceInfo.fromServiceInstance(new ErrorServiceImpl()).errorMapper(ERROR_MAPPER).build(), - opts -> new HttpGateway(opts.call(opts.call().errorMapper(ERROR_MAPPER)), ERROR_MAPPER)); - - private ErrorService service; - - @BeforeEach - void initService() { - service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); - } - - @Test - void shouldReturnSomeExceptionOnMono() { - StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayExtension.java b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayExtension.java deleted file mode 100644 index 098051647..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayExtension.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.scalecube.services.gateway.http; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.AbstractLocalGatewayExtension; -import io.scalecube.services.gateway.GatewayOptions; -import io.scalecube.services.gateway.transport.GatewayClientTransports; -import java.util.function.Function; - -class HttpLocalGatewayExtension extends AbstractLocalGatewayExtension { - - private static final String GATEWAY_ALIAS_NAME = "http"; - - HttpLocalGatewayExtension(Object serviceInstance) { - this(ServiceInfo.fromServiceInstance(serviceInstance).build()); - } - - HttpLocalGatewayExtension(ServiceInfo serviceInfo) { - this(serviceInfo, HttpGateway::new); - } - - HttpLocalGatewayExtension( - ServiceInfo serviceInfo, Function gatewaySupplier) { - super( - serviceInfo, - opts -> gatewaySupplier.apply(opts.id(GATEWAY_ALIAS_NAME)), - GatewayClientTransports::httpGatewayClientTransport); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayTest.java index 715711b3f..382b01b34 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/http/HttpLocalGatewayTest.java @@ -1,7 +1,12 @@ package io.scalecube.services.gateway.http; +import static io.scalecube.services.gateway.GatewayErrorMapperImpl.ERROR_MAPPER; import static org.junit.jupiter.api.Assertions.assertEquals; +import io.scalecube.services.Address; +import io.scalecube.services.Microservices; +import io.scalecube.services.ServiceCall; +import io.scalecube.services.ServiceInfo; import io.scalecube.services.api.Qualifier; import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.examples.EmptyGreetingRequest; @@ -11,30 +16,74 @@ import io.scalecube.services.examples.GreetingServiceImpl; import io.scalecube.services.exceptions.InternalServiceException; import io.scalecube.services.gateway.BaseTest; +import io.scalecube.services.gateway.ErrorService; +import io.scalecube.services.gateway.ErrorServiceImpl; +import io.scalecube.services.gateway.SomeException; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.http.HttpGatewayClientTransport; import java.time.Duration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; import reactor.test.StepVerifier; class HttpLocalGatewayTest extends BaseTest { private static final Duration TIMEOUT = Duration.ofSeconds(3); - @RegisterExtension - static HttpLocalGatewayExtension extension = - new HttpLocalGatewayExtension(new GreetingServiceImpl()); - - private GreetingService service; + private static Microservices gateway; + private static Address gatewayAddress; + private static StaticAddressRouter router; + + private ServiceCall serviceCall; + private GreetingService greetingService; + private ErrorService errorService; + + @BeforeAll + static void beforeAll() { + gateway = + Microservices.builder() + .gateway(options -> new HttpGateway.Builder().options(options.id("HTTP")).build()) + .services(new GreetingServiceImpl()) + .services( + ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) + .errorMapper(ERROR_MAPPER) + .build()) + .startAwait(); + gatewayAddress = gateway.gateway("HTTP").address(); + router = new StaticAddressRouter(gatewayAddress); + } @BeforeEach - void initService() { - service = extension.client().api(GreetingService.class); + void beforeEach() { + serviceCall = + new ServiceCall() + .router(router) + .transport(new HttpGatewayClientTransport.Builder().address(gatewayAddress).build()); + greetingService = serviceCall.api(GreetingService.class); + errorService = serviceCall.errorMapper(ERROR_MAPPER).api(ErrorService.class); + } + + @AfterEach + void afterEach() { + if (serviceCall != null) { + serviceCall.close(); + } + } + + @AfterAll + static void afterAll() { + if (gateway != null) { + gateway.close(); + } } @Test void shouldReturnSingleResponseWithSimpleRequest() { - StepVerifier.create(service.one("hello")) + StepVerifier.create(greetingService.one("hello")) .expectNext("Echo:hello") .expectComplete() .verify(TIMEOUT); @@ -43,7 +92,7 @@ void shouldReturnSingleResponseWithSimpleRequest() { @Test void shouldReturnSingleResponseWithSimpleLongDataRequest() { String data = new String(new char[500]); - StepVerifier.create(service.one(data)) + StepVerifier.create(greetingService.one(data)) .expectNext("Echo:" + data) .expectComplete() .verify(TIMEOUT); @@ -51,7 +100,7 @@ void shouldReturnSingleResponseWithSimpleLongDataRequest() { @Test void shouldReturnSingleResponseWithPojoRequest() { - StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) + StepVerifier.create(greetingService.pojoOne(new GreetingRequest("hello"))) .expectNextMatches(response -> "Echo:hello".equals(response.getText())) .expectComplete() .verify(TIMEOUT); @@ -59,7 +108,7 @@ void shouldReturnSingleResponseWithPojoRequest() { @Test void shouldReturnListResponseWithPojoRequest() { - StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) + StepVerifier.create(greetingService.pojoList(new GreetingRequest("hello"))) .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) .expectComplete() .verify(TIMEOUT); @@ -67,12 +116,12 @@ void shouldReturnListResponseWithPojoRequest() { @Test void shouldReturnNoContentWhenResponseIsEmpty() { - StepVerifier.create(service.emptyOne("hello")).expectComplete().verify(TIMEOUT); + StepVerifier.create(greetingService.emptyOne("hello")).expectComplete().verify(TIMEOUT); } @Test void shouldReturnInternalServerErrorWhenServiceFails() { - StepVerifier.create(service.failingOne("hello")) + StepVerifier.create(greetingService.failingOne("hello")) .expectErrorSatisfies( throwable -> { assertEquals(InternalServiceException.class, throwable.getClass()); @@ -83,12 +132,12 @@ void shouldReturnInternalServerErrorWhenServiceFails() { @Test void shouldSuccessfullyReuseServiceProxy() { - StepVerifier.create(service.one("hello")) + StepVerifier.create(greetingService.one("hello")) .expectNext("Echo:hello") .expectComplete() .verify(TIMEOUT); - StepVerifier.create(service.one("hello")) + StepVerifier.create(greetingService.one("hello")) .expectNext("Echo:hello") .expectComplete() .verify(TIMEOUT); @@ -96,7 +145,7 @@ void shouldSuccessfullyReuseServiceProxy() { @Test void shouldReturnNoEventOnNeverService() { - StepVerifier.create(service.neverOne("hi")) + StepVerifier.create(greetingService.neverOne("hi")) .expectSubscription() .expectNoEvent(Duration.ofSeconds(1)) .thenCancel() @@ -105,7 +154,7 @@ void shouldReturnNoEventOnNeverService() { @Test void shouldReturnOnEmptyGreeting() { - StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) + StepVerifier.create(greetingService.emptyGreeting(new EmptyGreetingRequest())) .expectSubscription() .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) .thenCancel() @@ -117,10 +166,16 @@ void shouldReturnOnEmptyMessageGreeting() { String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); ServiceMessage request = ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); - StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) + StepVerifier.create(serviceCall.requestOne(request, EmptyGreetingResponse.class)) .expectSubscription() .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) .thenCancel() .verify(); } + + @Disabled + @Test + void shouldReturnSomeException() { + StepVerifier.create(errorService.oneError()).expectError(SomeException.class).verify(TIMEOUT); + } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/ws/TestInputs.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/TestInputs.java similarity index 97% rename from services-gateway/src/test/java/io/scalecube/services/gateway/ws/TestInputs.java rename to services-gateway/src/test/java/io/scalecube/services/gateway/websocket/TestInputs.java index 44b7365cd..0b9cfc1e3 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/ws/TestInputs.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/TestInputs.java @@ -1,4 +1,4 @@ -package io.scalecube.services.gateway.ws; +package io.scalecube.services.gateway.websocket; public interface TestInputs { diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientConnectionTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientConnectionTest.java index a68ca78bd..e3544c5fe 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientConnectionTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientConnectionTest.java @@ -1,12 +1,10 @@ package io.scalecube.services.gateway.websocket; -import static io.scalecube.services.gateway.TestUtils.TIMEOUT; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.scalecube.services.Address; import io.scalecube.services.Microservices; import io.scalecube.services.ServiceCall; @@ -15,51 +13,36 @@ import io.scalecube.services.gateway.TestGatewaySessionHandler; import io.scalecube.services.gateway.TestService; import io.scalecube.services.gateway.TestServiceImpl; -import io.scalecube.services.gateway.TestUtils; -import io.scalecube.services.gateway.transport.GatewayClient; -import io.scalecube.services.gateway.transport.GatewayClientCodec; -import io.scalecube.services.gateway.transport.GatewayClientSettings; -import io.scalecube.services.gateway.transport.GatewayClientTransport; -import io.scalecube.services.gateway.transport.GatewayClientTransports; -import io.scalecube.services.gateway.transport.StaticAddressRouter; -import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClient; -import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClientSession; -import io.scalecube.services.gateway.ws.WebsocketGateway; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.websocket.WebsocketGatewayClientTransport; import io.scalecube.services.transport.rsocket.RSocketServiceTransport; import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; import java.io.IOException; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.time.Duration; import java.util.Collections; import java.util.UUID; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.netty.Connection; import reactor.test.StepVerifier; class WebsocketClientConnectionTest extends BaseTest { - public static final GatewayClientCodec CLIENT_CODEC = - GatewayClientTransports.WEBSOCKET_CLIENT_CODEC; - private static final AtomicInteger onCloseCounter = new AtomicInteger(); + private static final Duration TIMEOUT = Duration.ofSeconds(10); + private Microservices gateway; private Address gatewayAddress; - private Microservices service; - private GatewayClient client; - private TestGatewaySessionHandler sessionEventHandler; + private Microservices microservices; + private final TestGatewaySessionHandler sessionEventHandler = new TestGatewaySessionHandler(); + + private static final AtomicInteger onCloseCounter = new AtomicInteger(); @BeforeEach void beforEach() { - this.sessionEventHandler = new TestGatewaySessionHandler(); gateway = Microservices.builder() .discovery( @@ -68,12 +51,17 @@ void beforEach() { .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) .options(opts -> opts.metadata(serviceEndpoint))) .transport(RSocketServiceTransport::new) - .gateway(options -> new WebsocketGateway(options.id("WS"), sessionEventHandler)) + .gateway( + options -> + new WebsocketGateway.Builder() + .options(options.id("WS")) + .gatewayHandler(sessionEventHandler) + .build()) .startAwait(); gatewayAddress = gateway.gateway("WS").address(); - service = + microservices = Microservices.builder() .discovery( serviceEndpoint -> @@ -92,148 +80,90 @@ void beforEach() { @AfterEach void afterEach() { Flux.concat( - Mono.justOrEmpty(client).doOnNext(GatewayClient::close).flatMap(GatewayClient::onClose), Mono.justOrEmpty(gateway).map(Microservices::shutdown), - Mono.justOrEmpty(service).map(Microservices::shutdown)) + Mono.justOrEmpty(microservices).map(Microservices::shutdown)) .then() .block(); } @Test void testCloseServiceStreamAfterLostConnection() { - client = - new WebsocketGatewayClient( - GatewayClientSettings.builder().address(gatewayAddress).build(), CLIENT_CODEC); + try (ServiceCall serviceCall = serviceCall(gatewayAddress)) { + StepVerifier.create(serviceCall.api(TestService.class).manyNever().log("<<<")) + .thenAwait(Duration.ofSeconds(5)) + .then(serviceCall::close) + .expectError(IOException.class) + .verify(TIMEOUT); - ServiceCall serviceCall = - new ServiceCall() - .transport(new GatewayClientTransport(client)) - .router(new StaticAddressRouter(gatewayAddress)); - - StepVerifier.create(serviceCall.api(TestService.class).manyNever().log("<<< ")) - .thenAwait(Duration.ofSeconds(5)) - .then(() -> client.close()) - .then(() -> client.onClose().block()) - .expectError(IOException.class) - .verify(Duration.ofSeconds(10)); - - TestUtils.await(() -> onCloseCounter.get() == 1).block(TIMEOUT); - assertEquals(1, onCloseCounter.get()); + Mono.delay(Duration.ofMillis(100)) + .repeat(() -> onCloseCounter.get() != 1) + .then() + .block(TIMEOUT); + + assertEquals(1, onCloseCounter.get()); + } } @Test public void testCallRepeatedlyByInvalidAddress() { - Address invalidAddress = Address.create("localhost", 5050); - - client = - new WebsocketGatewayClient( - GatewayClientSettings.builder().address(invalidAddress).build(), CLIENT_CODEC); - - ServiceCall serviceCall = - new ServiceCall() - .transport(new GatewayClientTransport(client)) - .router(new StaticAddressRouter(invalidAddress)); - - for (int i = 0; i < 100; i++) { - StepVerifier.create(serviceCall.api(TestService.class).manyNever().log("<<< ")) - .thenAwait(Duration.ofSeconds(1)) - .expectError(IOException.class) - .verify(Duration.ofSeconds(10)); + try (ServiceCall serviceCall = serviceCall(Address.create("localhost", 5050))) { + for (int i = 0; i < 15; i++) { + StepVerifier.create(serviceCall.api(TestService.class).manyNever().log("<<<")) + .thenAwait(Duration.ofSeconds(1)) + .expectErrorSatisfies( + ex -> { + final Throwable cause = ex.getCause(); + assertNotNull(cause, "cause"); + assertInstanceOf(IOException.class, cause); + }) + .verify(TIMEOUT); + } } } @Test - public void testHandlerEvents() throws InterruptedException { - // Test Connect - client = - new WebsocketGatewayClient( - GatewayClientSettings.builder().address(gatewayAddress).build(), CLIENT_CODEC); - - TestService service = - new ServiceCall() - .transport(new GatewayClientTransport(client)) - .router(new StaticAddressRouter(gatewayAddress)) - .api(TestService.class); - - service.one("one").block(TIMEOUT); - sessionEventHandler.connLatch.await(3, TimeUnit.SECONDS); - Assertions.assertEquals(0, sessionEventHandler.connLatch.getCount()); - - sessionEventHandler.msgLatch.await(3, TimeUnit.SECONDS); - Assertions.assertEquals(0, sessionEventHandler.msgLatch.getCount()); - - client.close(); - sessionEventHandler.disconnLatch.await(3, TimeUnit.SECONDS); - Assertions.assertEquals(0, sessionEventHandler.disconnLatch.getCount()); + public void testHandlerEvents() throws Exception { + try (final ServiceCall serviceCall = serviceCall(gatewayAddress)) { + serviceCall.api(TestService.class).one("one").block(TIMEOUT); + assertTrue(sessionEventHandler.connLatch.await(3, TimeUnit.SECONDS)); + assertEquals(0, sessionEventHandler.connLatch.getCount()); + + assertTrue(sessionEventHandler.msgLatch.await(3, TimeUnit.SECONDS)); + assertEquals(0, sessionEventHandler.msgLatch.getCount()); + + serviceCall.close(); + assertTrue(sessionEventHandler.disconnLatch.await(3, TimeUnit.SECONDS)); + assertEquals(0, sessionEventHandler.disconnLatch.getCount()); + } } @Test - void testKeepalive() - throws InterruptedException, - NoSuchFieldException, - IllegalAccessException, - NoSuchMethodException, - InvocationTargetException { - - int expectedKeepalives = 3; - Duration keepAliveInterval = Duration.ofSeconds(1); - CountDownLatch keepaliveLatch = new CountDownLatch(expectedKeepalives); - client = - new WebsocketGatewayClient( - GatewayClientSettings.builder() - .address(gatewayAddress) - .keepAliveInterval(keepAliveInterval) - .build(), - CLIENT_CODEC); - - Method getOrConnect = WebsocketGatewayClient.class.getDeclaredMethod("getOrConnect"); - getOrConnect.setAccessible(true); - //noinspection unchecked - WebsocketGatewayClientSession session = - ((Mono) getOrConnect.invoke(client)).block(TIMEOUT); - Field connectionField = WebsocketGatewayClientSession.class.getDeclaredField("connection"); - connectionField.setAccessible(true); - Connection connection = (Connection) connectionField.get(session); - connection.addHandler( - new ChannelInboundHandlerAdapter() { - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof PongWebSocketFrame) { - ((PongWebSocketFrame) msg).release(); - keepaliveLatch.countDown(); - } else { - super.channelRead(ctx, msg); - } - } - }); - - keepaliveLatch.await( - keepAliveInterval.toMillis() * (expectedKeepalives + 1), TimeUnit.MILLISECONDS); - - assertEquals(0, keepaliveLatch.getCount()); - } + void testClientHeaders() { + final String headerKey = "secret-token"; + final String headerValue = UUID.randomUUID().toString(); - @Test - void testClientSettingsHeaders() { - String headerKey = "secret-token"; - String headerValue = UUID.randomUUID().toString(); - client = - new WebsocketGatewayClient( - GatewayClientSettings.builder() - .address(gatewayAddress) - .headers(Collections.singletonMap(headerKey, headerValue)) - .build(), - CLIENT_CODEC); - TestService service = + try (final ServiceCall serviceCall = new ServiceCall() - .transport(new GatewayClientTransport(client)) - .router(new StaticAddressRouter(gatewayAddress)) - .api(TestService.class); - - StepVerifier.create( - service.one("one").then(Mono.fromCallable(() -> sessionEventHandler.lastSession()))) - .assertNext(session -> assertEquals(headerValue, session.headers().get(headerKey))) - .expectComplete() - .verify(TIMEOUT); + .transport( + new WebsocketGatewayClientTransport.Builder() + .address(gatewayAddress) + .headers(Collections.singletonMap(headerKey, headerValue)) + .build()) + .router(new StaticAddressRouter(gatewayAddress))) { + StepVerifier.create( + serviceCall + .api(TestService.class) + .one("one") + .then(Mono.fromCallable(sessionEventHandler::lastSession))) + .assertNext(session -> assertEquals(headerValue, session.headers().get(headerKey))) + .expectComplete() + .verify(TIMEOUT); + } + } + + private static ServiceCall serviceCall(Address address) { + return new ServiceCall() + .transport(new WebsocketGatewayClientTransport.Builder().address(address).build()) + .router(new StaticAddressRouter(address)); } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientErrorMapperTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientErrorMapperTest.java deleted file mode 100644 index 6121f8d42..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientErrorMapperTest.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.scalecube.services.gateway.websocket; - -import static io.scalecube.services.gateway.TestUtils.TIMEOUT; -import static io.scalecube.services.gateway.exceptions.GatewayErrorMapperImpl.ERROR_MAPPER; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.BaseTest; -import io.scalecube.services.gateway.exceptions.ErrorService; -import io.scalecube.services.gateway.exceptions.ErrorServiceImpl; -import io.scalecube.services.gateway.exceptions.SomeException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import reactor.test.StepVerifier; - -class WebsocketClientErrorMapperTest extends BaseTest { - - @RegisterExtension - static WebsocketGatewayExtension extension = - new WebsocketGatewayExtension( - ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) - .errorMapper(ERROR_MAPPER) - .build()); - - private ErrorService service; - - @BeforeEach - void initService() { - service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); - } - - @Test - void shouldReturnSomeExceptionOnFlux() { - StepVerifier.create(service.manyError()).expectError(SomeException.class).verify(TIMEOUT); - } - - @Test - void shouldReturnSomeExceptionOnMono() { - StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientTest.java index 5309ba7a9..9abc137b5 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketClientTest.java @@ -1,51 +1,43 @@ package io.scalecube.services.gateway.websocket; -import io.netty.buffer.ByteBuf; +import static io.scalecube.services.gateway.GatewayErrorMapperImpl.ERROR_MAPPER; + import io.scalecube.services.Address; import io.scalecube.services.Microservices; import io.scalecube.services.ServiceCall; +import io.scalecube.services.ServiceInfo; import io.scalecube.services.annotations.Service; import io.scalecube.services.annotations.ServiceMethod; import io.scalecube.services.discovery.ScalecubeServiceDiscovery; import io.scalecube.services.gateway.BaseTest; +import io.scalecube.services.gateway.ErrorService; +import io.scalecube.services.gateway.ErrorServiceImpl; +import io.scalecube.services.gateway.SomeException; import io.scalecube.services.gateway.TestGatewaySessionHandler; -import io.scalecube.services.gateway.transport.GatewayClient; -import io.scalecube.services.gateway.transport.GatewayClientCodec; -import io.scalecube.services.gateway.transport.GatewayClientSettings; -import io.scalecube.services.gateway.transport.GatewayClientTransport; -import io.scalecube.services.gateway.transport.GatewayClientTransports; -import io.scalecube.services.gateway.transport.StaticAddressRouter; -import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClient; -import io.scalecube.services.gateway.ws.WebsocketGateway; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.websocket.WebsocketGatewayClientTransport; import io.scalecube.services.transport.rsocket.RSocketServiceTransport; import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; import java.time.Duration; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.netty.resources.LoopResources; import reactor.test.StepVerifier; class WebsocketClientTest extends BaseTest { - public static final GatewayClientCodec CLIENT_CODEC = - GatewayClientTransports.WEBSOCKET_CLIENT_CODEC; + private static final Duration TIMEOUT = Duration.ofSeconds(10); private static Microservices gateway; private static Address gatewayAddress; - private static Microservices service; - private static GatewayClient client; - private static LoopResources loopResources; + private static Microservices microservices; @BeforeAll static void beforeAll() { - loopResources = LoopResources.create("websocket-gateway-client"); - gateway = Microservices.builder() .discovery( @@ -55,11 +47,16 @@ static void beforeAll() { .options(opts -> opts.metadata(serviceEndpoint))) .transport(RSocketServiceTransport::new) .gateway( - options -> new WebsocketGateway(options.id("WS"), new TestGatewaySessionHandler())) + options -> + new WebsocketGateway.Builder() + .options(options.id("WS")) + .gatewayHandler(new TestGatewaySessionHandler()) + .build()) .startAwait(); + gatewayAddress = gateway.gateway("WS").address(); - service = + microservices = Microservices.builder() .discovery( serviceEndpoint -> @@ -70,60 +67,63 @@ static void beforeAll() { opts -> opts.seedMembers(gateway.discoveryAddress().toString()))) .transport(RSocketServiceTransport::new) .services(new TestServiceImpl()) + .services( + ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) + .errorMapper(ERROR_MAPPER) + .build()) .startAwait(); } - @AfterEach - void afterEach() { - final GatewayClient client = WebsocketClientTest.client; - if (client != null) { - client.close(); - } - } - @AfterAll static void afterAll() { - final GatewayClient client = WebsocketClientTest.client; - if (client != null) { - client.close(); - } - Flux.concat( Mono.justOrEmpty(gateway).map(Microservices::shutdown), - Mono.justOrEmpty(service).map(Microservices::shutdown)) + Mono.justOrEmpty(microservices).map(Microservices::shutdown)) .then() .block(); + } - if (loopResources != null) { - loopResources.disposeLater().block(); + @Test + void testMessageSequence() { + try (ServiceCall serviceCall = serviceCall(gatewayAddress)) { + final int count = 1000; + StepVerifier.create(serviceCall.api(TestService.class).many(count) /*.log("<<<")*/) + .expectNextSequence(IntStream.range(0, count).boxed().collect(Collectors.toList())) + .expectComplete() + .verify(TIMEOUT); } } @Test - void testMessageSequence() { - client = - new WebsocketGatewayClient( - GatewayClientSettings.builder().address(gatewayAddress).build(), - CLIENT_CODEC, - loopResources); - - ServiceCall serviceCall = - new ServiceCall() - .transport(new GatewayClientTransport(client)) - .router(new StaticAddressRouter(gatewayAddress)); - - int count = 1000; - - StepVerifier.create(serviceCall.api(TestService.class).many(count) /*.log("<<< ")*/) - .expectNextSequence(IntStream.range(0, count).boxed().collect(Collectors.toList())) - .expectComplete() - .verify(Duration.ofSeconds(10)); + void shouldReturnSomeExceptionOnFlux() { + try (final ServiceCall serviceCall = serviceCall(gatewayAddress)) { + final ErrorService errorService = + serviceCall.errorMapper(ERROR_MAPPER).api(ErrorService.class); + StepVerifier.create(errorService.manyError()) + .expectError(SomeException.class) + .verify(TIMEOUT); + } + } + + @Test + void shouldReturnSomeExceptionOnMono() { + try (final ServiceCall serviceCall = serviceCall(gatewayAddress)) { + final ErrorService errorService = + serviceCall.errorMapper(ERROR_MAPPER).api(ErrorService.class); + StepVerifier.create(errorService.oneError()).expectError(SomeException.class).verify(TIMEOUT); + } + } + + private static ServiceCall serviceCall(final Address address) { + return new ServiceCall() + .transport(new WebsocketGatewayClientTransport.Builder().address(address).build()) + .router(new StaticAddressRouter(address)); } @Service public interface TestService { - @ServiceMethod("many") + @ServiceMethod Flux many(int count); } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayAuthTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayAuthTest.java new file mode 100644 index 000000000..eae73b47a --- /dev/null +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayAuthTest.java @@ -0,0 +1,179 @@ +package io.scalecube.services.gateway.websocket; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.scalecube.services.Address; +import io.scalecube.services.Microservices; +import io.scalecube.services.ServiceCall; +import io.scalecube.services.api.ServiceMessage; +import io.scalecube.services.exceptions.ForbiddenException; +import io.scalecube.services.exceptions.UnauthorizedException; +import io.scalecube.services.gateway.AuthRegistry; +import io.scalecube.services.gateway.GatewaySessionHandlerImpl; +import io.scalecube.services.gateway.SecuredService; +import io.scalecube.services.gateway.SecuredServiceImpl; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.websocket.WebsocketGatewayClientTransport; +import java.time.Duration; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +public class WebsocketGatewayAuthTest { + + private static final Duration TIMEOUT = Duration.ofSeconds(3); + + private static final String ALLOWED_USER = "VASYA_PUPKIN"; + private static final Set ALLOWED_USERS = + new HashSet<>(Collections.singletonList(ALLOWED_USER)); + + private static final AuthRegistry AUTH_REGISTRY = new AuthRegistry(ALLOWED_USERS); + private static Microservices gateway; + + private ServiceCall serviceCall; + private SecuredService securedService; + + @BeforeAll + static void beforeAll() { + gateway = + Microservices.builder() + .gateway( + options -> + new WebsocketGateway.Builder() + .options(options.id("WS")) + .gatewayHandler(new GatewaySessionHandlerImpl(AUTH_REGISTRY)) + .build()) + .services(new SecuredServiceImpl(AUTH_REGISTRY)) + .startAwait(); + } + + @BeforeEach + void beforeEach() { + final Address gatewayAddress = gateway.gateway("WS").address(); + + serviceCall = + new ServiceCall() + .router(new StaticAddressRouter(gatewayAddress)) + .transport( + new WebsocketGatewayClientTransport.Builder().address(gatewayAddress).build()); + + securedService = serviceCall.api(SecuredService.class); + } + + @AfterEach + void afterEach() { + if (serviceCall != null) { + serviceCall.close(); + } + } + + @AfterAll + static void afterAll() { + if (gateway != null) { + gateway.close(); + } + } + + @Test + void createSessionSuccessfully() { + StepVerifier.create(serviceCall.requestOne(createSessionRequest(ALLOWED_USER), String.class)) + .expectNextCount(1) + .expectComplete() + .verify(); + } + + @Test + void createSessionForbiddenUser() { + StepVerifier.create( + serviceCall.requestOne(createSessionRequest("fake" + ALLOWED_USER), String.class)) + .expectErrorSatisfies( + th -> { + ForbiddenException e = (ForbiddenException) th; + assertEquals(403, e.errorCode(), "error code"); + assertTrue(e.getMessage().contains("User not allowed to use this service")); + }) + .verify(); + } + + @Test + void securedMethodNotAuthenticated() { + StepVerifier.create(securedService.requestOne("echo")) + .expectErrorSatisfies( + th -> { + UnauthorizedException e = (UnauthorizedException) th; + assertEquals(401, e.errorCode(), "Authentication failed"); + assertTrue(e.getMessage().contains("Authentication failed")); + }) + .verify(); + } + + @Test + void securedMethodAuthenticated() { + // authenticate session + serviceCall.requestOne(createSessionRequest(ALLOWED_USER), String.class).block(TIMEOUT); + // call secured service + final String req = "echo"; + StepVerifier.create(securedService.requestOne(req)) + .expectNextMatches(resp -> resp.equals(ALLOWED_USER + "@" + req)) + .expectComplete() + .verify(); + } + + @Test + void securedMethodAuthenticatedInvalidUser() { + // authenticate session + StepVerifier.create( + serviceCall.requestOne(createSessionRequest("fake" + ALLOWED_USER), String.class)) + .expectErrorSatisfies(th -> assertInstanceOf(ForbiddenException.class, th)) + .verify(); + // call secured service + final String req = "echo"; + StepVerifier.create(securedService.requestOne(req)) + .expectErrorSatisfies( + th -> { + UnauthorizedException e = (UnauthorizedException) th; + assertEquals(401, e.errorCode(), "Authentication failed"); + assertTrue(e.getMessage().contains("Authentication failed")); + }) + .verify(); + } + + @Test + void securedMethodNotAuthenticatedRequestStream() { + StepVerifier.create(securedService.requestN(10)) + .expectErrorSatisfies( + th -> { + UnauthorizedException e = (UnauthorizedException) th; + assertEquals(401, e.errorCode(), "Authentication failed"); + assertTrue(e.getMessage().contains("Authentication failed")); + }) + .verify(); + } + + @Test + void securedMethodAuthenticatedReqStream() { + // authenticate session + serviceCall.requestOne(createSessionRequest(ALLOWED_USER), String.class).block(TIMEOUT); + // call secured service + Integer times = 10; + StepVerifier.create(securedService.requestN(times)) + .expectNextCount(10) + .expectComplete() + .verify(); + } + + private static ServiceMessage createSessionRequest(String username) { + return ServiceMessage.builder() + .qualifier("/" + SecuredService.NAMESPACE + "/createSession") + .data(username) + .build(); + } +} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayExtension.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayExtension.java deleted file mode 100644 index 04ccf1b5d..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayExtension.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.scalecube.services.gateway.websocket; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.AbstractGatewayExtension; -import io.scalecube.services.gateway.transport.GatewayClientTransports; -import io.scalecube.services.gateway.ws.WebsocketGateway; - -class WebsocketGatewayExtension extends AbstractGatewayExtension { - - private static final String GATEWAY_ALIAS_NAME = "ws"; - - WebsocketGatewayExtension(Object serviceInstance) { - this(ServiceInfo.fromServiceInstance(serviceInstance).build()); - } - - WebsocketGatewayExtension(ServiceInfo serviceInfo) { - super( - serviceInfo, - opts -> new WebsocketGateway(opts.id(GATEWAY_ALIAS_NAME)), - GatewayClientTransports::websocketGatewayClientTransport); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayTest.java index acf5b2a76..1e10f9148 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketGatewayTest.java @@ -1,9 +1,14 @@ package io.scalecube.services.gateway.websocket; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static io.scalecube.services.gateway.GatewayErrorMapperImpl.ERROR_MAPPER; +import io.scalecube.services.Address; +import io.scalecube.services.Microservices; +import io.scalecube.services.ServiceCall; +import io.scalecube.services.ServiceInfo; import io.scalecube.services.api.Qualifier; import io.scalecube.services.api.ServiceMessage; +import io.scalecube.services.discovery.ScalecubeServiceDiscovery; import io.scalecube.services.examples.EmptyGreetingRequest; import io.scalecube.services.examples.EmptyGreetingResponse; import io.scalecube.services.examples.GreetingRequest; @@ -11,36 +16,103 @@ import io.scalecube.services.examples.GreetingService; import io.scalecube.services.examples.GreetingServiceImpl; import io.scalecube.services.exceptions.InternalServiceException; -import io.scalecube.services.exceptions.ServiceUnavailableException; import io.scalecube.services.gateway.BaseTest; +import io.scalecube.services.gateway.ErrorService; +import io.scalecube.services.gateway.ErrorServiceImpl; +import io.scalecube.services.gateway.SomeException; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.websocket.WebsocketGatewayClientTransport; +import io.scalecube.services.transport.rsocket.RSocketServiceTransport; +import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; import java.time.Duration; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; import reactor.test.StepVerifier; class WebsocketGatewayTest extends BaseTest { private static final Duration TIMEOUT = Duration.ofSeconds(3); - @RegisterExtension - static WebsocketGatewayExtension extension = - new WebsocketGatewayExtension(new GreetingServiceImpl()); - - private GreetingService service; + private static Microservices gateway; + private static Address gatewayAddress; + private static StaticAddressRouter router; + private static Microservices microservices; + + private ServiceCall serviceCall; + private GreetingService greetingService; + private ErrorService errorService; + + @BeforeAll + static void beforeAll() { + gateway = + Microservices.builder() + .discovery( + serviceEndpoint -> + new ScalecubeServiceDiscovery() + .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) + .options(opts -> opts.metadata(serviceEndpoint))) + .transport(RSocketServiceTransport::new) + .gateway(options -> new WebsocketGateway.Builder().options(options.id("WS")).build()) + .startAwait(); + + gatewayAddress = gateway.gateway("WS").address(); + router = new StaticAddressRouter(gatewayAddress); + + microservices = + Microservices.builder() + .discovery( + serviceEndpoint -> + new ScalecubeServiceDiscovery() + .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) + .options(opts -> opts.metadata(serviceEndpoint)) + .membership( + opts -> opts.seedMembers(gateway.discoveryAddress().toString()))) + .transport(RSocketServiceTransport::new) + .services(new GreetingServiceImpl()) + .services( + ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) + .errorMapper(ERROR_MAPPER) + .build()) + .startAwait(); + } @BeforeEach - void initService() { - service = extension.client().api(GreetingService.class); + void beforeEach() { + serviceCall = + new ServiceCall() + .router(router) + .transport( + new WebsocketGatewayClientTransport.Builder().address(gatewayAddress).build()); + greetingService = serviceCall.api(GreetingService.class); + errorService = serviceCall.errorMapper(ERROR_MAPPER).api(ErrorService.class); + } + + @AfterEach + void afterEach() { + if (serviceCall != null) { + serviceCall.close(); + } + } + + @AfterAll + static void afterAll() { + if (gateway != null) { + gateway.close(); + } + if (microservices != null) { + microservices.close(); + } } @Test void shouldReturnSingleResponseWithSimpleRequest() { - StepVerifier.create(service.one("hello")) + StepVerifier.create(greetingService.one("hello")) .expectNext("Echo:hello") .expectComplete() .verify(TIMEOUT); @@ -49,7 +121,7 @@ void shouldReturnSingleResponseWithSimpleRequest() { @Test void shouldReturnSingleResponseWithSimpleLongDataRequest() { String data = new String(new char[500]); - StepVerifier.create(service.one(data)) + StepVerifier.create(greetingService.one(data)) .expectNext("Echo:" + data) .expectComplete() .verify(TIMEOUT); @@ -57,7 +129,7 @@ void shouldReturnSingleResponseWithSimpleLongDataRequest() { @Test void shouldReturnSingleResponseWithPojoRequest() { - StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) + StepVerifier.create(greetingService.pojoOne(new GreetingRequest("hello"))) .expectNextMatches(response -> "Echo:hello".equals(response.getText())) .expectComplete() .verify(TIMEOUT); @@ -65,7 +137,7 @@ void shouldReturnSingleResponseWithPojoRequest() { @Test void shouldReturnListResponseWithPojoRequest() { - StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) + StepVerifier.create(greetingService.pojoList(new GreetingRequest("hello"))) .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) .expectComplete() .verify(TIMEOUT); @@ -79,7 +151,7 @@ void shouldReturnManyResponsesWithSimpleRequest() { .mapToObj(i -> "Greeting (" + i + ") to: hello") .collect(Collectors.toList()); - StepVerifier.create(service.many("hello").take(expectedResponseNum)) + StepVerifier.create(greetingService.many("hello").take(expectedResponseNum)) .expectNextSequence(expected) .expectComplete() .verify(TIMEOUT); @@ -93,34 +165,23 @@ void shouldReturnManyResponsesWithPojoRequest() { .mapToObj(i -> new GreetingResponse("Greeting (" + i + ") to: hello")) .collect(Collectors.toList()); - StepVerifier.create(service.pojoMany(new GreetingRequest("hello")).take(expectedResponseNum)) + StepVerifier.create( + greetingService.pojoMany(new GreetingRequest("hello")).take(expectedResponseNum)) .expectNextSequence(expected) .expectComplete() .verify(TIMEOUT); } - @Test - void shouldReturnExceptionWhenServiceIsDown() { - extension.shutdownServices(); - - StepVerifier.create(service.one("hello")) - .expectErrorMatches( - throwable -> - throwable instanceof ServiceUnavailableException - && throwable.getMessage().startsWith("No reachable member with such service")) - .verify(TIMEOUT); - } - @Test void shouldReturnErrorDataWhenServiceFails() { - StepVerifier.create(service.failingOne("hello")) + StepVerifier.create(greetingService.failingOne("hello")) .expectErrorMatches(throwable -> throwable instanceof InternalServiceException) .verify(TIMEOUT); } @Test void shouldReturnErrorDataWhenRequestDataIsEmpty() { - StepVerifier.create(service.one(null)) + StepVerifier.create(greetingService.one(null)) .expectErrorMatches( throwable -> "Expected service request data of type: class java.lang.String, but received: null" @@ -130,7 +191,7 @@ void shouldReturnErrorDataWhenRequestDataIsEmpty() { @Test void shouldReturnNoEventOnNeverService() { - StepVerifier.create(service.neverOne("hi")) + StepVerifier.create(greetingService.neverOne("hi")) .expectSubscription() .expectNoEvent(Duration.ofSeconds(1)) .thenCancel() @@ -139,7 +200,7 @@ void shouldReturnNoEventOnNeverService() { @Test void shouldReturnOnEmptyGreeting() { - StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) + StepVerifier.create(greetingService.emptyGreeting(new EmptyGreetingRequest())) .expectSubscription() .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) .thenCancel() @@ -151,19 +212,20 @@ void shouldReturnOnEmptyMessageGreeting() { String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); ServiceMessage request = ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); - StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) + StepVerifier.create(serviceCall.requestOne(request, EmptyGreetingResponse.class)) .expectSubscription() .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) .thenCancel() .verify(); } - @Disabled("https://github.com/scalecube/scalecube-services/issues/742") - public void testManyStreamBlockFirst() { - for (int i = 0; i < 100; i++) { - //noinspection ConstantConditions - long first = service.manyStream(30L).filter(k -> k != 0).take(1).blockFirst(); - assertEquals(1, first); - } + @Test + void shouldReturnSomeExceptionOnFlux() { + StepVerifier.create(errorService.manyError()).expectError(SomeException.class).verify(TIMEOUT); + } + + @Test + void shouldReturnSomeExceptionOnMono() { + StepVerifier.create(errorService.oneError()).expectError(SomeException.class).verify(TIMEOUT); } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayAuthTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayAuthTest.java deleted file mode 100644 index a7b61a5b2..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayAuthTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package io.scalecube.services.gateway.websocket; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.scalecube.services.api.ServiceMessage; -import io.scalecube.services.exceptions.ForbiddenException; -import io.scalecube.services.exceptions.UnauthorizedException; -import io.scalecube.services.gateway.AuthRegistry; -import io.scalecube.services.gateway.SecuredService; -import io.scalecube.services.gateway.SecuredServiceImpl; -import java.time.Duration; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import reactor.test.StepVerifier; - -public class WebsocketLocalGatewayAuthTest { - - private static final Duration TIMEOUT = Duration.ofSeconds(3); - - private static final String ALLOWED_USER = "VASYA_PUPKIN"; - private static final Set ALLOWED_USERS = - new HashSet<>(Collections.singletonList(ALLOWED_USER)); - - private static final AuthRegistry AUTH_REG = new AuthRegistry(ALLOWED_USERS); - - @RegisterExtension - static WebsocketLocalWithAuthExtension extension = - new WebsocketLocalWithAuthExtension(new SecuredServiceImpl(AUTH_REG), AUTH_REG); - - private SecuredService clientService; - - private static ServiceMessage createSessionReq(String username) { - return ServiceMessage.builder() - .qualifier("/" + SecuredService.NS + "/createSession") - .data(username) - .build(); - } - - @BeforeEach - void initService() { - clientService = extension.client().api(SecuredService.class); - } - - @Test - void testCreateSession_succ() { - StepVerifier.create(extension.client().requestOne(createSessionReq(ALLOWED_USER), String.class)) - .expectNextCount(1) - .expectComplete() - .verify(); - } - - @Test - void testCreateSession_forbiddenUser() { - StepVerifier.create( - extension.client().requestOne(createSessionReq("fake" + ALLOWED_USER), String.class)) - .expectErrorSatisfies( - th -> { - ForbiddenException e = (ForbiddenException) th; - assertEquals(403, e.errorCode(), "error code"); - assertTrue(e.getMessage().contains("User not allowed to use this service")); - }) - .verify(); - } - - @Test - void testCallSecuredMethod_notAuthenticated() { - StepVerifier.create(clientService.requestOne("echo")) - .expectErrorSatisfies( - th -> { - UnauthorizedException e = (UnauthorizedException) th; - assertEquals(401, e.errorCode(), "Authentication failed"); - assertTrue(e.getMessage().contains("Authentication failed")); - }) - .verify(); - } - - @Test - void testCallSecuredMethod_authenticated() { - // authenticate session - extension.client().requestOne(createSessionReq(ALLOWED_USER), String.class).block(TIMEOUT); - // call secured service - final String req = "echo"; - StepVerifier.create(clientService.requestOne(req)) - .expectNextMatches(resp -> resp.equals(ALLOWED_USER + "@" + req)) - .expectComplete() - .verify(); - } - - @Test - void testCallSecuredMethod_authenticatedInvalidUser() { - // authenticate session - StepVerifier.create( - extension.client().requestOne(createSessionReq("fake" + ALLOWED_USER), String.class)) - .expectErrorSatisfies(th -> assertTrue(th instanceof ForbiddenException)) - .verify(); - // call secured service - final String req = "echo"; - StepVerifier.create(clientService.requestOne(req)) - .expectErrorSatisfies( - th -> { - UnauthorizedException e = (UnauthorizedException) th; - assertEquals(401, e.errorCode(), "Authentication failed"); - assertTrue(e.getMessage().contains("Authentication failed")); - }) - .verify(); - } - - @Test - void testCallSecuredMethod_notAuthenticatedRequestStream() { - StepVerifier.create(clientService.requestN(10)) - .expectErrorSatisfies( - th -> { - UnauthorizedException e = (UnauthorizedException) th; - assertEquals(401, e.errorCode(), "Authentication failed"); - assertTrue(e.getMessage().contains("Authentication failed")); - }) - .verify(); - } - - @Test - void testCallSecuredMethod_authenticatedReqStream() { - // authenticate session - extension.client().requestOne(createSessionReq(ALLOWED_USER), String.class).block(TIMEOUT); - // call secured service - Integer times = 10; - StepVerifier.create(clientService.requestN(times)) - .expectNextCount(10) - .expectComplete() - .verify(); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayErrorMapperTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayErrorMapperTest.java deleted file mode 100644 index 8a5de923b..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayErrorMapperTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.scalecube.services.gateway.websocket; - -import static io.scalecube.services.gateway.TestUtils.TIMEOUT; -import static io.scalecube.services.gateway.exceptions.GatewayErrorMapperImpl.ERROR_MAPPER; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.BaseTest; -import io.scalecube.services.gateway.exceptions.ErrorService; -import io.scalecube.services.gateway.exceptions.ErrorServiceImpl; -import io.scalecube.services.gateway.exceptions.SomeException; -import io.scalecube.services.gateway.ws.WebsocketGateway; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import reactor.test.StepVerifier; - -class WebsocketLocalGatewayErrorMapperTest extends BaseTest { - - @RegisterExtension - static WebsocketLocalGatewayExtension extension = - new WebsocketLocalGatewayExtension( - ServiceInfo.fromServiceInstance(new ErrorServiceImpl()).errorMapper(ERROR_MAPPER).build(), - opts -> - new WebsocketGateway(opts.call(opts.call().errorMapper(ERROR_MAPPER)), ERROR_MAPPER)); - - private ErrorService service; - - @BeforeEach - void initService() { - service = extension.client().errorMapper(ERROR_MAPPER).api(ErrorService.class); - } - - @Test - void shouldReturnSomeExceptionOnFlux() { - StepVerifier.create(service.manyError()).expectError(SomeException.class).verify(TIMEOUT); - } - - @Test - void shouldReturnSomeExceptionOnMono() { - StepVerifier.create(service.oneError()).expectError(SomeException.class).verify(TIMEOUT); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayExtension.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayExtension.java deleted file mode 100644 index 77ebc8c8b..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayExtension.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.scalecube.services.gateway.websocket; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.AbstractLocalGatewayExtension; -import io.scalecube.services.gateway.GatewayOptions; -import io.scalecube.services.gateway.transport.GatewayClientTransports; -import io.scalecube.services.gateway.ws.WebsocketGateway; -import java.util.function.Function; - -class WebsocketLocalGatewayExtension extends AbstractLocalGatewayExtension { - - private static final String GATEWAY_ALIAS_NAME = "ws"; - - WebsocketLocalGatewayExtension(Object serviceInstance) { - this(ServiceInfo.fromServiceInstance(serviceInstance).build()); - } - - WebsocketLocalGatewayExtension(ServiceInfo serviceInfo) { - this(serviceInfo, WebsocketGateway::new); - } - - WebsocketLocalGatewayExtension( - ServiceInfo serviceInfo, Function gatewaySupplier) { - super( - serviceInfo, - opts -> gatewaySupplier.apply(opts.id(GATEWAY_ALIAS_NAME)), - GatewayClientTransports::websocketGatewayClientTransport); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayTest.java index 12955cf7e..52cce13b4 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalGatewayTest.java @@ -1,7 +1,11 @@ package io.scalecube.services.gateway.websocket; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static io.scalecube.services.gateway.GatewayErrorMapperImpl.ERROR_MAPPER; +import io.scalecube.services.Address; +import io.scalecube.services.Microservices; +import io.scalecube.services.ServiceCall; +import io.scalecube.services.ServiceInfo; import io.scalecube.services.api.Qualifier; import io.scalecube.services.api.ServiceMessage; import io.scalecube.services.examples.EmptyGreetingRequest; @@ -12,33 +16,82 @@ import io.scalecube.services.examples.GreetingServiceImpl; import io.scalecube.services.exceptions.InternalServiceException; import io.scalecube.services.gateway.BaseTest; +import io.scalecube.services.gateway.ErrorService; +import io.scalecube.services.gateway.ErrorServiceImpl; +import io.scalecube.services.gateway.SomeException; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.websocket.WebsocketGatewayClientTransport; import java.time.Duration; import java.util.List; import java.util.stream.Collectors; import java.util.stream.IntStream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; import reactor.test.StepVerifier; class WebsocketLocalGatewayTest extends BaseTest { private static final Duration TIMEOUT = Duration.ofSeconds(3); - @RegisterExtension - static WebsocketLocalGatewayExtension extension = - new WebsocketLocalGatewayExtension(new GreetingServiceImpl()); - - private GreetingService service; + private static Microservices gateway; + private static Address gatewayAddress; + private static StaticAddressRouter router; + + private ServiceCall serviceCall; + private GreetingService greetingService; + private ErrorService errorService; + + @BeforeAll + static void beforeAll() { + gateway = + Microservices.builder() + .gateway( + options -> + new WebsocketGateway.Builder() + .options(options.id("WS").call(options.call().errorMapper(ERROR_MAPPER))) + .errorMapper(ERROR_MAPPER) + .build()) + .services(new GreetingServiceImpl()) + .services( + ServiceInfo.fromServiceInstance(new ErrorServiceImpl()) + .errorMapper(ERROR_MAPPER) + .build()) + .startAwait(); + gatewayAddress = gateway.gateway("WS").address(); + router = new StaticAddressRouter(gatewayAddress); + } @BeforeEach - void initService() { - service = extension.client().api(GreetingService.class); + void beforeEach() { + serviceCall = + new ServiceCall() + .router(router) + .transport( + new WebsocketGatewayClientTransport.Builder().address(gatewayAddress).build()); + greetingService = serviceCall.api(GreetingService.class); + errorService = serviceCall.errorMapper(ERROR_MAPPER).api(ErrorService.class); + } + + @AfterEach + void afterEach() { + if (serviceCall != null) { + serviceCall.close(); + } + } + + @AfterAll + static void afterAll() { + if (gateway != null) { + gateway.close(); + } } @Test void shouldReturnSingleResponseWithSimpleRequest() { - StepVerifier.create(service.one("hello")) + StepVerifier.create(greetingService.one("hello")) .expectNext("Echo:hello") .expectComplete() .verify(TIMEOUT); @@ -47,7 +100,7 @@ void shouldReturnSingleResponseWithSimpleRequest() { @Test void shouldReturnSingleResponseWithSimpleLongDataRequest() { String data = new String(new char[500]); - StepVerifier.create(service.one(data)) + StepVerifier.create(greetingService.one(data)) .expectNext("Echo:" + data) .expectComplete() .verify(TIMEOUT); @@ -55,7 +108,7 @@ void shouldReturnSingleResponseWithSimpleLongDataRequest() { @Test void shouldReturnSingleResponseWithPojoRequest() { - StepVerifier.create(service.pojoOne(new GreetingRequest("hello"))) + StepVerifier.create(greetingService.pojoOne(new GreetingRequest("hello"))) .expectNextMatches(response -> "Echo:hello".equals(response.getText())) .expectComplete() .verify(TIMEOUT); @@ -63,7 +116,7 @@ void shouldReturnSingleResponseWithPojoRequest() { @Test void shouldReturnListResponseWithPojoRequest() { - StepVerifier.create(service.pojoList(new GreetingRequest("hello"))) + StepVerifier.create(greetingService.pojoList(new GreetingRequest("hello"))) .expectNextMatches(response -> "Echo:hello".equals(response.get(0).getText())) .expectComplete() .verify(TIMEOUT); @@ -77,7 +130,7 @@ void shouldReturnManyResponsesWithSimpleRequest() { .mapToObj(i -> "Greeting (" + i + ") to: hello") .collect(Collectors.toList()); - StepVerifier.create(service.many("hello").take(expectedResponseNum)) + StepVerifier.create(greetingService.many("hello").take(expectedResponseNum)) .expectNextSequence(expected) .expectComplete() .verify(TIMEOUT); @@ -91,7 +144,8 @@ void shouldReturnManyResponsesWithPojoRequest() { .mapToObj(i -> new GreetingResponse("Greeting (" + i + ") to: hello")) .collect(Collectors.toList()); - StepVerifier.create(service.pojoMany(new GreetingRequest("hello")).take(expectedResponseNum)) + StepVerifier.create( + greetingService.pojoMany(new GreetingRequest("hello")).take(expectedResponseNum)) .expectNextSequence(expected) .expectComplete() .verify(TIMEOUT); @@ -99,14 +153,14 @@ void shouldReturnManyResponsesWithPojoRequest() { @Test void shouldReturnErrorDataWhenServiceFails() { - StepVerifier.create(service.failingOne("hello")) + StepVerifier.create(greetingService.failingOne("hello")) .expectErrorMatches(throwable -> throwable instanceof InternalServiceException) .verify(TIMEOUT); } @Test void shouldReturnErrorDataWhenRequestDataIsEmpty() { - StepVerifier.create(service.one(null)) + StepVerifier.create(greetingService.one(null)) .expectErrorMatches( throwable -> "Expected service request data of type: class java.lang.String, but received: null" @@ -116,7 +170,7 @@ void shouldReturnErrorDataWhenRequestDataIsEmpty() { @Test void shouldReturnNoEventOnNeverService() { - StepVerifier.create(service.neverOne("hi")) + StepVerifier.create(greetingService.neverOne("hi")) .expectSubscription() .expectNoEvent(Duration.ofSeconds(1)) .thenCancel() @@ -125,7 +179,7 @@ void shouldReturnNoEventOnNeverService() { @Test void shouldReturnOnEmptyGreeting() { - StepVerifier.create(service.emptyGreeting(new EmptyGreetingRequest())) + StepVerifier.create(greetingService.emptyGreeting(new EmptyGreetingRequest())) .expectSubscription() .expectNextMatches(resp -> resp instanceof EmptyGreetingResponse) .thenCancel() @@ -137,7 +191,7 @@ void shouldReturnOnEmptyMessageGreeting() { String qualifier = Qualifier.asString(GreetingService.NAMESPACE, "empty/wrappedPojo"); ServiceMessage request = ServiceMessage.builder().qualifier(qualifier).data(new EmptyGreetingRequest()).build(); - StepVerifier.create(extension.client().requestOne(request, EmptyGreetingResponse.class)) + StepVerifier.create(serviceCall.requestOne(request, EmptyGreetingResponse.class)) .expectSubscription() .expectNextMatches(resp -> resp.data() instanceof EmptyGreetingResponse) .thenCancel() @@ -145,11 +199,12 @@ void shouldReturnOnEmptyMessageGreeting() { } @Test - public void testManyStreamBlockFirst() { - for (int i = 0; i < 100; i++) { - //noinspection ConstantConditions - long first = service.manyStream(30L).filter(k -> k != 0).take(1).blockFirst(); - assertEquals(1, first); - } + void shouldReturnSomeExceptionOnFlux() { + StepVerifier.create(errorService.manyError()).expectError(SomeException.class).verify(TIMEOUT); + } + + @Test + void shouldReturnSomeExceptionOnMono() { + StepVerifier.create(errorService.oneError()).expectError(SomeException.class).verify(TIMEOUT); } } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalWithAuthExtension.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalWithAuthExtension.java deleted file mode 100644 index a81688e69..000000000 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketLocalWithAuthExtension.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.scalecube.services.gateway.websocket; - -import io.scalecube.services.ServiceInfo; -import io.scalecube.services.gateway.AbstractLocalGatewayExtension; -import io.scalecube.services.gateway.AuthRegistry; -import io.scalecube.services.gateway.GatewaySessionHandlerImpl; -import io.scalecube.services.gateway.transport.GatewayClientTransports; -import io.scalecube.services.gateway.ws.WebsocketGateway; - -public class WebsocketLocalWithAuthExtension extends AbstractLocalGatewayExtension { - - private static final String GATEWAY_ALIAS_NAME = "ws"; - - WebsocketLocalWithAuthExtension(Object serviceInstance, AuthRegistry authReg) { - this(ServiceInfo.fromServiceInstance(serviceInstance).build(), authReg); - } - - WebsocketLocalWithAuthExtension(ServiceInfo serviceInfo, AuthRegistry authReg) { - super( - serviceInfo, - opts -> - new WebsocketGateway( - opts.id(GATEWAY_ALIAS_NAME), new GatewaySessionHandlerImpl(authReg)), - GatewayClientTransports::websocketGatewayClientTransport); - } -} diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketServerTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketServerTest.java index 8b0b5bcc4..cbbabbaca 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketServerTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketServerTest.java @@ -1,113 +1,69 @@ package io.scalecube.services.gateway.websocket; -import io.netty.buffer.ByteBuf; import io.scalecube.services.Address; import io.scalecube.services.Microservices; import io.scalecube.services.ServiceCall; import io.scalecube.services.annotations.Service; import io.scalecube.services.annotations.ServiceMethod; -import io.scalecube.services.discovery.ScalecubeServiceDiscovery; import io.scalecube.services.gateway.BaseTest; import io.scalecube.services.gateway.TestGatewaySessionHandler; -import io.scalecube.services.gateway.transport.GatewayClient; -import io.scalecube.services.gateway.transport.GatewayClientCodec; -import io.scalecube.services.gateway.transport.GatewayClientSettings; -import io.scalecube.services.gateway.transport.GatewayClientTransport; -import io.scalecube.services.gateway.transport.GatewayClientTransports; -import io.scalecube.services.gateway.transport.StaticAddressRouter; -import io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClient; -import io.scalecube.services.gateway.ws.WebsocketGateway; -import io.scalecube.services.transport.rsocket.RSocketServiceTransport; -import io.scalecube.transport.netty.websocket.WebsocketTransportFactory; +import io.scalecube.services.gateway.client.StaticAddressRouter; +import io.scalecube.services.gateway.client.websocket.WebsocketGatewayClientTransport; import java.time.Duration; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.netty.resources.LoopResources; import reactor.test.StepVerifier; class WebsocketServerTest extends BaseTest { - public static final GatewayClientCodec CLIENT_CODEC = - GatewayClientTransports.WEBSOCKET_CLIENT_CODEC; - private static Microservices gateway; private static Address gatewayAddress; - private static GatewayClient client; - private static LoopResources loopResources; @BeforeAll static void beforeAll() { - loopResources = LoopResources.create("websocket-gateway-client"); - gateway = Microservices.builder() - .discovery( - serviceEndpoint -> - new ScalecubeServiceDiscovery() - .transport(cfg -> cfg.transportFactory(new WebsocketTransportFactory())) - .options(opts -> opts.metadata(serviceEndpoint))) - .transport(RSocketServiceTransport::new) .gateway( - options -> new WebsocketGateway(options.id("WS"), new TestGatewaySessionHandler())) - .transport(RSocketServiceTransport::new) + options -> + new WebsocketGateway.Builder() + .options(options.id("WS")) + .gatewayHandler(new TestGatewaySessionHandler()) + .build()) .services(new TestServiceImpl()) .startAwait(); gatewayAddress = gateway.gateway("WS").address(); } - @AfterEach - void afterEach() { - final GatewayClient client = WebsocketServerTest.client; - if (client != null) { - client.close(); - } - } - @AfterAll static void afterAll() { - final GatewayClient client = WebsocketServerTest.client; - if (client != null) { - client.close(); - } - - Mono.justOrEmpty(gateway).map(Microservices::shutdown).then().block(); - - if (loopResources != null) { - loopResources.disposeLater().block(); + if (gateway != null) { + gateway.close(); } } @Test void testMessageSequence() { - client = - new WebsocketGatewayClient( - GatewayClientSettings.builder().address(gatewayAddress).build(), - CLIENT_CODEC, - loopResources); - - ServiceCall serviceCall = + try (ServiceCall serviceCall = new ServiceCall() - .transport(new GatewayClientTransport(client)) - .router(new StaticAddressRouter(gatewayAddress)); - - int count = 1000; - - StepVerifier.create(serviceCall.api(TestService.class).many(count) /*.log("<<< ")*/) - .expectNextSequence(IntStream.range(0, count).boxed().collect(Collectors.toList())) - .expectComplete() - .verify(Duration.ofSeconds(10)); + .transport( + new WebsocketGatewayClientTransport.Builder().address(gatewayAddress).build()) + .router(new StaticAddressRouter(gatewayAddress))) { + int count = 1000; + StepVerifier.create(serviceCall.api(TestService.class).many(count) /*.log("<<<")*/) + .expectNextSequence(IntStream.range(0, count).boxed().collect(Collectors.toList())) + .expectComplete() + .verify(Duration.ofSeconds(10)); + } } @Service public interface TestService { - @ServiceMethod("many") + @ServiceMethod Flux many(int count); } diff --git a/services-gateway/src/test/java/io/scalecube/services/gateway/ws/WebsocketServiceMessageCodecTest.java b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketServiceMessageCodecTest.java similarity index 96% rename from services-gateway/src/test/java/io/scalecube/services/gateway/ws/WebsocketServiceMessageCodecTest.java rename to services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketServiceMessageCodecTest.java index a8aeb56c3..066ad1684 100644 --- a/services-gateway/src/test/java/io/scalecube/services/gateway/ws/WebsocketServiceMessageCodecTest.java +++ b/services-gateway/src/test/java/io/scalecube/services/gateway/websocket/WebsocketServiceMessageCodecTest.java @@ -1,10 +1,10 @@ -package io.scalecube.services.gateway.ws; +package io.scalecube.services.gateway.websocket; -import static io.scalecube.services.gateway.ws.GatewayMessages.DATA_FIELD; -import static io.scalecube.services.gateway.ws.GatewayMessages.INACTIVITY_FIELD; -import static io.scalecube.services.gateway.ws.GatewayMessages.QUALIFIER_FIELD; -import static io.scalecube.services.gateway.ws.GatewayMessages.SIGNAL_FIELD; -import static io.scalecube.services.gateway.ws.GatewayMessages.STREAM_ID_FIELD; +import static io.scalecube.services.gateway.websocket.GatewayMessages.DATA_FIELD; +import static io.scalecube.services.gateway.websocket.GatewayMessages.INACTIVITY_FIELD; +import static io.scalecube.services.gateway.websocket.GatewayMessages.QUALIFIER_FIELD; +import static io.scalecube.services.gateway.websocket.GatewayMessages.SIGNAL_FIELD; +import static io.scalecube.services.gateway.websocket.GatewayMessages.STREAM_ID_FIELD; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/services-transport-parent/services-transport-rsocket/src/main/java/io/scalecube/services/transport/rsocket/RSocketClientChannel.java b/services-transport-parent/services-transport-rsocket/src/main/java/io/scalecube/services/transport/rsocket/RSocketClientChannel.java index c339c4ad4..5f4497157 100644 --- a/services-transport-parent/services-transport-rsocket/src/main/java/io/scalecube/services/transport/rsocket/RSocketClientChannel.java +++ b/services-transport-parent/services-transport-rsocket/src/main/java/io/scalecube/services/transport/rsocket/RSocketClientChannel.java @@ -8,16 +8,12 @@ import io.scalecube.services.transport.api.ClientChannel; import java.lang.reflect.Type; import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.channel.AbortedException; public class RSocketClientChannel implements ClientChannel { - private static final Logger LOGGER = LoggerFactory.getLogger(RSocketClientChannel.class); - private final Mono rsocket; private final ServiceMessageCodec messageCodec; diff --git a/services-transport-parent/services-transport-rsocket/src/main/java/io/scalecube/services/transport/rsocket/RSocketClientTransport.java b/services-transport-parent/services-transport-rsocket/src/main/java/io/scalecube/services/transport/rsocket/RSocketClientTransport.java index 41f2efb7e..36c41472b 100644 --- a/services-transport-parent/services-transport-rsocket/src/main/java/io/scalecube/services/transport/rsocket/RSocketClientTransport.java +++ b/services-transport-parent/services-transport-rsocket/src/main/java/io/scalecube/services/transport/rsocket/RSocketClientTransport.java @@ -30,8 +30,7 @@ public class RSocketClientTransport implements ClientTransport { private static final Logger LOGGER = LoggerFactory.getLogger(RSocketClientTransport.class); - private final ThreadLocal>> rsockets = - ThreadLocal.withInitial(ConcurrentHashMap::new); + private final Map> rsockets = new ConcurrentHashMap<>(); private final CredentialsSupplier credentialsSupplier; private final ConnectionSetupCodec connectionSetupCodec; @@ -63,7 +62,7 @@ public RSocketClientTransport( @Override public ClientChannel create(ServiceReference serviceReference) { - final Map> monoMap = rsockets.get(); // keep reference for threadsafety + final Map> monoMap = this.rsockets; // keep reference for threadsafety final Address address = serviceReference.address(); Mono mono = monoMap.computeIfAbsent( @@ -148,4 +147,16 @@ private UnauthorizedException toUnauthorizedException(Throwable th) { return new UnauthorizedException(th); } } + + @Override + public void close() { + rsockets.forEach( + (address, socketMono) -> + socketMono.subscribe( + RSocket::dispose, + throwable -> { + // no-op + })); + rsockets.clear(); + } }