All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.scalecube.services.gateway.transport.websocket.WebsocketGatewayClient Maven / Gradle / Ivy

package io.scalecube.services.gateway.transport.websocket;

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.MonoProcessor;
import reactor.netty.Connection;
import reactor.netty.http.client.HttpClient;
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";
  private static final String SIGNAL = "sig";

  @SuppressWarnings("rawtypes")
  private static final AtomicReferenceFieldUpdater
      websocketMonoUpdater =
          AtomicReferenceFieldUpdater.newUpdater(
              WebsocketGatewayClient.class, Mono.class, "websocketMono");

  private final GatewayClientCodec codec;
  private final GatewayClientSettings settings;
  private final HttpClient httpClient;
  private final AtomicLong sidCounter = new AtomicLong();
  private final LoopResources loopResources;
  private final MonoProcessor close = MonoProcessor.create();
  private final MonoProcessor onClose = MonoProcessor.create();

  @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 = settings;
    this.codec = codec;
    this.loopResources = LoopResources.create("websocket-gateway-client");

    httpClient =
        HttpClient.newConnection()
            .followRedirect(settings.followRedirect())
            .tcpConfiguration(
                tcpClient -> {
                  if (settings.sslProvider() != null) {
                    tcpClient = tcpClient.secure(settings.sslProvider());
                  }
                  return tcpClient
                      .wiretap(settings.wiretap())
                      .runOn(loopResources)
                      .host(settings.host())
                      .port(settings.port());
                });

    // Setup cleanup
    close
        .then(doClose())
        .doFinally(s -> onClose.onComplete())
        .doOnTerminate(() -> LOGGER.info("Closed WebsocketGatewayClient resources"))
        .subscribe(
            null, ex -> LOGGER.warn("Exception occurred on WebsocketGatewayClient close: " + ex));
  }

  @Override
  public Mono requestResponse(ServiceMessage request) {
    return Mono.defer(
        () -> {
          long sid = sidCounter.incrementAndGet();
          return getOrConnect()
              .flatMap(
                  session ->
                      session
                          .send(encodeRequest(request, sid), sid)
                          .doOnSubscribe(s -> LOGGER.debug("Sending request {}", request))
                          .then(session.newMonoProcessor(sid))
                          .doOnCancel(() -> handleCancel(sid, session))
                          .doFinally(s -> session.removeProcessor(sid)));
        });
  }

  @Override
  public Flux requestStream(ServiceMessage request) {
    return Flux.defer(
        () -> {
          long sid = sidCounter.incrementAndGet();
          return getOrConnect()
              .flatMapMany(
                  session ->
                      session
                          .send(encodeRequest(request, sid), sid)
                          .doOnSubscribe(s -> LOGGER.debug("Sending request {}", request))
                          .thenMany(session.newUnicastProcessor(sid))
                          .doOnCancel(() -> handleCancel(sid, session))
                          .doFinally(s -> session.removeProcessor(sid)));
        });
  }

  @Override
  public Flux requestChannel(Flux requests) {
    return Flux.error(
        new UnsupportedOperationException(
            "requestChannel is not supported by WebSocket transport implementation"));
  }

  @Override
  public void close() {
    close.onComplete();
  }

  @Override
  public Mono onClose() {
    return onClose;
  }

  private Mono doClose() {
    return Mono.defer(loopResources::disposeLater);
  }

  public GatewayClientCodec getCodec() {
    return codec;
  }

  private Mono getOrConnect() {
    // noinspection unchecked
    return Mono.defer(() -> 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 -> {
              WebsocketSession session = new WebsocketSession(codec, connection);
              LOGGER.info("Created {} on {}:{}", session, settings.host(), settings.port());
              // setup shutdown hook
              session
                  .onClose()
                  .doOnTerminate(
                      () -> {
                        websocketMonoUpdater.getAndSet(this, null); // clear reference
                        LOGGER.info(
                            "Closed {} on {}:{}", session, settings.host(), settings.port());
                      })
                  .subscribe(
                      null,
                      th ->
                          LOGGER.warn(
                              "Exception on closing session={}, cause: {}",
                              session.id(),
                              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 void handleCancel(long sid, WebsocketSession session) {
    ByteBuf byteBuf =
        codec.encode(
            ServiceMessage.builder()
                .header(STREAM_ID, sid)
                .header(SIGNAL, Signal.CANCEL.codeAsString())
                .build());
    session
        .send(byteBuf, sid)
        .subscribe(
            null,
            th ->
                LOGGER.error(
                    "Exception on sending CANCEL signal for session={}", session.id(), th));
  }

  private ByteBuf encodeRequest(ServiceMessage message, long sid) {
    return codec.encode(ServiceMessage.from(message).header(STREAM_ID, sid).build());
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy