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

org.jboss.pnc.restclient.websocket.VertxWebSocketClient Maven / Gradle / Ivy

There is a newer version: 3.0.4
Show newest version
/**
 * JBoss, Home of Professional Open Source.
 * Copyright 2014-2022 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jboss.pnc.restclient.websocket;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.jboss.pnc.client.RemoteResourceException;
import org.jboss.pnc.common.json.JsonOutputConverterMapper;
import org.jboss.pnc.dto.Build;
import org.jboss.pnc.dto.BuildPushResult;
import org.jboss.pnc.dto.GroupBuild;
import org.jboss.pnc.dto.ProductMilestoneCloseResult;
import org.jboss.pnc.dto.notification.BuildChangedNotification;
import org.jboss.pnc.dto.notification.BuildConfigurationCreation;
import org.jboss.pnc.dto.notification.BuildPushResultNotification;
import org.jboss.pnc.dto.notification.GroupBuildChangedNotification;
import org.jboss.pnc.dto.notification.Notification;
import org.jboss.pnc.dto.notification.ProductMilestoneCloseResultNotification;
import org.jboss.pnc.dto.notification.RepositoryCreationFailure;
import org.jboss.pnc.dto.notification.SCMRepositoryCreationSuccess;
import org.jboss.pnc.enums.BuildStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.WebSocket;

/**
 * @author Jan Michalov
 */
public class VertxWebSocketClient implements WebSocketClient, AutoCloseable {

    private static final Logger log = LoggerFactory.getLogger(VertxWebSocketClient.class);

    private static final ObjectMapper objectMapper = getObjectMapper();

    /**
     * Almost identical version of {@link JsonOutputConverterMapper} but without constant error messages on missing
     * openshift class
     * 
     * @return JSON mapper
     */
    private static ObjectMapper getObjectMapper() {
        return new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL)
                .registerModule(new Jdk8Module())
                .registerModule(new JavaTimeModule())
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                .disable(SerializationFeature.FAIL_ON_UNWRAPPED_TYPE_IDENTIFIERS);
    }

    private Vertx vertx;

    private HttpClient httpClient;

    private WebSocket webSocketConnection;

    /**
     * vert.x timer id which periodically sends pings to the WS server
     */
    private long periodicPingTimerId = -1;

    /**
     * delay between individual pings in ms.
     *
     * default: 2 sec
     */
    private int pingDelays = 2000;

    /**
     * amount of time we allow the WS server to be unresponsive to the pings until we consider reconnection
     *
     * default: 20 sec
     */
    private int maxUnresponsivenessTime = 20000;

    /**
     * how many pings were left unanswered.
     *
     * if pingPongDifference > (maxUnresponsivenessTime/pingDelays) is true, we start reconnecting.
     * (maxUnresponsivenessTime/pingDelays) equals to upper limit of unanswered pings.
     */
    private AtomicLong pingPongDifference = new AtomicLong(0);

    /**
     * use concurrent version since we may modify that list concurrently
     */
    private Set dispatchers = ConcurrentHashMap.newKeySet();

    private Map, Supplier> singleNotificationFutures = new ConcurrentHashMap<>();

    /**
     * maximum amount of time in milliseconds taken between retries
     * 

* default: 10 min */ private int upperLimitForRetry = 600000; private int numberOfRetries = 0; /** * a multiplier that increases delay between reconnect attempts */ private float delayMultiplier = 1.5F; /** * amount of milliseconds client waits before attempting to reconnect */ private int initialDelay = 250; private int reconnectDelay; /** * timeout we wait on first connection to WS server or on reconnection */ private final int connectTimeout = 5000; public VertxWebSocketClient() { reconnectDelay = initialDelay; } public VertxWebSocketClient( int upperLimitForRetry, int initialDelay, float delayMultiplier, int pingDelays, int maxUnresponsivenessTime) { this.delayMultiplier = delayMultiplier; this.upperLimitForRetry = upperLimitForRetry; this.initialDelay = initialDelay; reconnectDelay = initialDelay; this.pingDelays = pingDelays; this.maxUnresponsivenessTime = maxUnresponsivenessTime; } @Override public CompletableFuture connect(String webSocketServerUrl) { if (webSocketServerUrl == null) { throw new IllegalArgumentException("WebSocketServerUrl is null"); } final URI serverURI; try { serverURI = new URI(webSocketServerUrl); } catch (URISyntaxException e) { throw new IllegalArgumentException("WebSocketServerUrl is not valid URI", e); } if (this.vertx == null) { this.vertx = Vertx.vertx(); HttpClientOptions options = new HttpClientOptions(); options.setKeepAlive(false).setConnectTimeout(connectTimeout); this.httpClient = vertx.createHttpClient(options); } if (webSocketConnection != null && !webSocketConnection.isClosed()) { log.trace("Already connected."); return CompletableFuture.completedFuture(null); } // in case no port was given, default to http port 80 int port = serverURI.getPort() == -1 ? 80 : serverURI.getPort(); CompletableFuture future = new CompletableFuture<>(); httpClient.webSocket(port, serverURI.getHost(), serverURI.getPath(), result -> { if (result.succeeded()) { log.debug("Connection to WebSocket server: " + webSocketServerUrl + " successful."); resetDefaults(); webSocketConnection = result.result(); webSocketConnection.textMessageHandler(this::dispatch); webSocketConnection.closeHandler((ignore) -> connectionClosed(webSocketServerUrl)); startPingPong(webSocketServerUrl); // Async operation complete future.complete(null); } else { log.error("Connection to WebSocket server: " + webSocketServerUrl + " unsuccessful.", result.cause()); // if there was a request to reconnect through retries, try to reconnect for possible network issues if (numberOfRetries > 0) { connectionLost(webSocketServerUrl); } future.completeExceptionally(result.cause()); } }); return future; } private void dispatch(String message) { dispatchers.forEach((dispatcher) -> dispatcher.accept(message)); } private void connectionClosed(String webSocketServerUrl) { log.warn("WebSocket connection was remotely closed, will retry in: " + reconnectDelay + " milliseconds."); retryConnection(webSocketServerUrl); } private void connectionLost(String webSocketServerUrl) { log.warn( "WebSocket connection lost. Possible VPN/Network issues, will retry in: " + reconnectDelay + " milliseconds."); retryConnection(webSocketServerUrl); } private void connectionUnreachable(String webSocketServerUrl) { log.warn( "WebSocket server is unreachable. Possible VPN/Network issues, will retry in: " + reconnectDelay + " milliseconds."); retryConnection(webSocketServerUrl); } private void manuallyCloseConnection() { if (webSocketConnection != null && !webSocketConnection.isClosed()) { log.trace("Manually closing WS connection."); webSocketConnection.close(); } } private void retryConnection(String webSocketServerUrl) { numberOfRetries++; vertx.setTimer(reconnectDelay, (timerId) -> connectAndReset(webSocketServerUrl)); // don't exceed upper limit for retry if (reconnectDelay * delayMultiplier > upperLimitForRetry) reconnectDelay = upperLimitForRetry; else reconnectDelay *= delayMultiplier; } private CompletableFuture connectAndReset(String webSocketServerUrl) { log.warn("Trying to reconnect. Number of retries: " + numberOfRetries); return connect(webSocketServerUrl).thenRun(this::runReconnectChecksOnSingles).thenRun(this::resetDefaults); } /** * Run reconnect checks (f.e. invoke REST) on associated notifications and complete them if check succeeds (returns * non-null value) */ private void runReconnectChecksOnSingles() { singleNotificationFutures.forEach((key, value) -> { if (!key.isDone()) { Notification notification = value.get(); if ((notification != null)) { key.complete(notification); } } }); } private void startPingPong(String webSocketServerUrl) { webSocketConnection.pongHandler(this::handlePong); periodicPingTimerId = vertx.setPeriodic(pingDelays, (timerId) -> ping(timerId, webSocketServerUrl)); } private void ping(long timerId, String webSocketServerUrl) { if (pingPongDifference.get() > (maxUnresponsivenessTime / pingDelays)) { // cancel itself to avoid sending pings during reconnections if (vertx.cancelTimer(timerId)) { periodicPingTimerId = -1; } // unresponsive server still has opened connection even though it does not respond manuallyCloseConnection(); connectionUnreachable(webSocketServerUrl); return; } log.trace("Sending ping to WS server: " + webSocketServerUrl); pingPongDifference.incrementAndGet(); webSocketConnection.writePing(Buffer.buffer()); } private void handlePong(Buffer ignore) { log.trace("Received pong from WS server."); pingPongDifference.decrementAndGet(); } private void resetDefaults() { pingPongDifference.set(0); reconnectDelay = initialDelay; numberOfRetries = 0; } @Override public CompletableFuture disconnect() { if (webSocketConnection == null || webSocketConnection.isClosed()) { // already disconnected return CompletableFuture.completedFuture(null); } CompletableFuture future = new CompletableFuture<>(); vertx.cancelTimer(periodicPingTimerId); webSocketConnection.closeHandler(null); webSocketConnection.close((result) -> { if (result.succeeded()) { log.debug("Connection to WebSocket server successfully closed."); future.complete(null); } else { log.error("Connection to WebSocket server unsuccessfully closed.", result.cause()); future.completeExceptionally(result.cause()); } }); return future.whenComplete((x, y) -> vertx.close((nothing) -> clearVertx())); } private void clearVertx() { this.vertx = null; this.httpClient = null; this.webSocketConnection = null; } @Override public ListenerUnsubscriber onMessage( Class notificationClass, Consumer listener, Predicate... filters) throws ConnectionClosedException { if (webSocketConnection == null || webSocketConnection.isClosed()) { throw new ConnectionClosedException("Connection to WebSocket is closed."); } // add JSON message mapping before executing the listener Dispatcher dispatcher = (stringMessage) -> { T notification; try { notification = objectMapper.readValue(stringMessage, notificationClass); for (Predicate filter : filters) { if (filter != null && !filter.test(notification)) { // does not satisfy a predicate return; } } } catch (JsonProcessingException e) { // could not parse to particular class of notification, unknown or different type of notification // ignoring the message return; } listener.accept(notification); }; dispatchers.add(dispatcher); return () -> dispatchers.remove(dispatcher); } @Override public CompletableFuture catchSingleNotification( Class notificationClass, Supplier reconnectCheck, Predicate... filters) { CompletableFuture future = new CompletableFuture<>(); // returns null on incorrect message Supplier reconnectWithTestCheck = () -> { T t = reconnectCheck.get(); for (Predicate filter : filters) { if (t == null || !filter.test(t)) { return null; } } return t; }; singleNotificationFutures .put((CompletableFuture) future, (Supplier) reconnectWithTestCheck); ListenerUnsubscriber unsubscriber = null; try { unsubscriber = onMessage(notificationClass, future::complete, filters); } catch (ConnectionClosedException e) { future.completeExceptionally(e); // in this case we have to set unsubscriber manually to avoid NPE unsubscriber = () -> {}; } final ListenerUnsubscriber finalUnsubscriber = unsubscriber; return future.whenComplete((notification, throwable) -> finalUnsubscriber.run()); } // NOTIFICATION LISTENERS @Override public ListenerUnsubscriber onBuildChangedNotification( Consumer onNotification, Predicate... filters) throws ConnectionClosedException { return onMessage(BuildChangedNotification.class, onNotification, filters); } @Override public ListenerUnsubscriber onBuildConfigurationCreation( Consumer onNotification, Predicate... filters) throws ConnectionClosedException { return onMessage(BuildConfigurationCreation.class, onNotification, filters); } @Override public ListenerUnsubscriber onBuildPushResult( Consumer onNotification, Predicate... filters) throws ConnectionClosedException { return onMessage(BuildPushResultNotification.class, onNotification, filters); } @Override public ListenerUnsubscriber onGroupBuildChangedNotification( Consumer onNotification, Predicate... filters) throws ConnectionClosedException { return onMessage(GroupBuildChangedNotification.class, onNotification, filters); } @Override public ListenerUnsubscriber onRepositoryCreationFailure( Consumer onNotification, Predicate... filters) throws ConnectionClosedException { return onMessage(RepositoryCreationFailure.class, onNotification, filters); } @Override public ListenerUnsubscriber onSCMRepositoryCreationSuccess( Consumer onNotification, Predicate... filters) throws ConnectionClosedException { return onMessage(SCMRepositoryCreationSuccess.class, onNotification, filters); } @Override public ListenerUnsubscriber onProductMilestoneCloseResult( Consumer onNotification, Predicate... filters) throws ConnectionClosedException { return onMessage(ProductMilestoneCloseResultNotification.class, onNotification, filters); } // NO RECONNECTS private CompletableFuture catchSingleNotification( Class notificationClass, Predicate... filters) { return catchSingleNotification(notificationClass, () -> null, filters); } @Override public CompletableFuture catchBuildChangedNotification( Predicate... filters) { return catchSingleNotification(BuildChangedNotification.class, filters); } @Override public CompletableFuture catchBuildConfigurationCreation( Predicate... filters) { return catchSingleNotification(BuildConfigurationCreation.class, filters); } @Override public CompletableFuture catchBuildPushResult( Predicate... filters) { return catchSingleNotification(BuildPushResultNotification.class, filters); } @Override public CompletableFuture catchGroupBuildChangedNotification( Predicate... filters) { return catchSingleNotification(GroupBuildChangedNotification.class, filters); } @Override public CompletableFuture catchProductMilestoneCloseResult( Predicate... filters) { return catchSingleNotification(ProductMilestoneCloseResultNotification.class, filters); } @Override public CompletableFuture catchRepositoryCreationFailure( Predicate... filters) { return catchSingleNotification(RepositoryCreationFailure.class, filters); } @Override public CompletableFuture catchSCMRepositoryCreationSuccess( Predicate... filters) { return catchSingleNotification(SCMRepositoryCreationSuccess.class, filters); } // WITH RECONNECTS @Override public CompletableFuture catchBuildChangedNotification( FallbackRequestSupplier reconnectSupplier, Predicate... filters) { return catchSingleNotification( BuildChangedNotification.class, () -> mockBuildNotification(reconnectSupplier), filters); } private BuildChangedNotification mockBuildNotification(FallbackRequestSupplier fallback) { Build build = null; try { build = fallback.get(); } catch (RemoteResourceException exception) { log.warn("Failsafe reconnection failed.", exception); return null; } return build == null ? null : new BuildChangedNotification(BuildStatus.NEW, build); } @Override public CompletableFuture catchGroupBuildChangedNotification( FallbackRequestSupplier reconnectSupplier, Predicate... filters) { return catchSingleNotification( GroupBuildChangedNotification.class, () -> mockGroupNotification(reconnectSupplier), filters); } private GroupBuildChangedNotification mockGroupNotification(FallbackRequestSupplier fallback) { GroupBuild groupBuild = null; try { groupBuild = fallback.get(); } catch (RemoteResourceException exception) { log.warn("Failsafe reconnection failed.", exception); return null; } return groupBuild == null ? null : new GroupBuildChangedNotification(groupBuild); } @Override public CompletableFuture catchBuildPushResult( FallbackRequestSupplier reconnectSupplier, Predicate... filters) { return catchSingleNotification( BuildPushResultNotification.class, () -> mockBuildPushNotification(reconnectSupplier), filters); } private BuildPushResultNotification mockBuildPushNotification(FallbackRequestSupplier fallback) { BuildPushResult pushResult = null; try { pushResult = fallback.get(); } catch (RemoteResourceException exception) { log.warn("Failsafe reconnection failed.", exception); return null; } return pushResult == null ? null : new BuildPushResultNotification(pushResult); } @Override public CompletableFuture catchProductMilestoneCloseResult( FallbackRequestSupplier reconnectSupplier, Predicate... filters) { return catchSingleNotification( ProductMilestoneCloseResultNotification.class, () -> mockMilestoneCloseNotification(reconnectSupplier), filters); } private ProductMilestoneCloseResultNotification mockMilestoneCloseNotification( FallbackRequestSupplier fallback) { ProductMilestoneCloseResult pushResult = null; try { pushResult = fallback.get(); } catch (RemoteResourceException exception) { log.warn("Failsafe reconnection failed.", exception); return null; } return pushResult == null ? null : new ProductMilestoneCloseResultNotification(pushResult); } @Override public void close() throws Exception { disconnect().join(); if (vertx != null) vertx.close(); } /** * TODO: Use Java 11 Cleaner class instead after Orchestrator migration to Java 11 * * @throws Throwable throwable * @see Java 11 * Cleaner */ @Override @Deprecated protected void finalize() throws Throwable { try { try { // attempt to properly disconnect disconnect().get(); } finally { // always close vertx if (vertx != null) vertx.close(); } } finally { // always finalize super.finalize(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy