org.opendaylight.jsonrpc.bus.spi.AbstractReconnectingClient Maven / Gradle / Ivy
/*
* Copyright (c) 2018 Lumina Networks, Inc. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.jsonrpc.bus.spi;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.Uninterruptibles;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.ScheduledFuture;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import org.opendaylight.jsonrpc.bus.api.ClientSession;
import org.opendaylight.jsonrpc.bus.api.SessionType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Client endpoint which handles reconnect in case of failure.
*
* @author Richard Kosegi
* @since Mar 6, 2018
*/
public abstract class AbstractReconnectingClient extends AbstractSession implements ClientSession {
private static final Logger LOG = LoggerFactory.getLogger(AbstractReconnectingClient.class);
private final Bootstrap clientBootstrap;
private ScheduledFuture> reconnectFuture;
protected volatile ConnectionState state = ConnectionState.INITIAL;
private final ChannelFutureListener connectListener = new ConnectListener();
private final ChannelFutureListener closeListener = new CloseListener();
protected final AbstractChannelInitializer channelInitializer;
private ReconnectStrategy reconnectStrategy;
private final AtomicReference isFirstConnectionAttempt = new AtomicReference<>(true);
private static final Set RECONNECT_STATES = ImmutableSet.builder()
.add(ConnectionState.DONE)
.build();
public AbstractReconnectingClient(String uri, int defaultPort, Bootstrap clientBootstrap,
AbstractChannelInitializer channelInitializer, SessionType sessionType) {
super(uri, defaultPort, sessionType);
reconnectStrategy = ReconnectStrategies.fixedStartegy(1000);
this.channelInitializer = Objects.requireNonNull(channelInitializer);
this.clientBootstrap = Objects.requireNonNull(clientBootstrap).clone().handler(channelInitializer);
}
/*
* If we are not yet done, then schedule reconnect
*/
@SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD", justification = "False positive, this method is called")
private void scheduleReconnect() {
if (ConnectionState.DONE != state) {
changeConnectionState(ConnectionState.INITIAL);
reconnectFuture = clientBootstrap.config().group().schedule(this::connectInternal,
reconnectStrategy.timeout(), TimeUnit.MILLISECONDS);
}
}
/**
* Attempts to connect to server.
*
* @see Channel#connect(java.net.SocketAddress)
*/
protected void connectInternal() {
if (state == ConnectionState.CONNECTED || state == ConnectionState.CONNECTING) {
return;
}
if (state == ConnectionState.DONE) {
throw new IllegalStateException("Client closed already : " + address);
}
LOG.debug("(Re)connecting to {} ", address);
changeConnectionState(ConnectionState.CONNECTING);
clientBootstrap.handler(channelInitializer).connect(address).addListener(connectListener);
}
/**
* Invoked when {@link Channel} is closed.
*/
private final class CloseListener implements ChannelFutureListener {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!RECONNECT_STATES.contains(state)) {
LOG.debug("Scheduling reconnect because state is {}@{}", state, hashCode());
changeConnectionState(ConnectionState.INITIAL);
scheduleReconnect();
}
}
}
/**
* Connection callback. Invoked when
*
* - Connection is normally established.
* - Connection attempt failed.
*
*
* @see Bootstrap#connect(java.net.SocketAddress)
* @see ChannelFuture#addListener(io.netty.util.concurrent.GenericFutureListener)
*/
private final class ConnectListener implements ChannelFutureListener {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isSuccess()) {
// Connection established
channelFuture = future;
changeConnectionState(ConnectionState.CONNECTED);
reconnectStrategy.reset();
future.channel().closeFuture().addListener(closeListener);
} else {
// log warning only for first connection failure
if (isFirstConnectionAttempt.getAndSet(false)) {
LOG.warn("Connection attempt to '{}' failed", address, future.cause());
} else {
LOG.trace("Connection attempt to '{}' failed", address, future.cause());
}
scheduleReconnect();
}
}
}
private void changeConnectionState(ConnectionState newState) {
if (state != newState) {
LOG.debug("Changing connection state from {} to {} [{}]@{}", state, newState,
channelFuture != null ? channelFuture.channel() : "N/A", hashCode());
state = newState;
}
}
/**
* Signals that we are done and perform cleanup of client's {@link Channel}.
*
* @return {@link Future} which is completed once we are done with cleanup.
*/
@SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION")
protected java.util.concurrent.Future closeChannel() {
changeConnectionState(ConnectionState.DONE);
if (reconnectFuture != null) {
reconnectFuture.cancel(true);
reconnectFuture = null;
}
if (channelFuture != null) {
return channelFuture.channel().close();
}
return Futures.immediateFuture(null);
}
/**
* Check is client is ready for communication.
*
* @return true if and only if client is ready for communication
*/
@Override
public boolean isReady() {
return state == ConnectionState.CONNECTED && handshakeFinished();
}
protected boolean handshakeFinished() {
return channelFuture.channel().attr(CommonConstants.ATTR_HANDSHAKE_DONE).get();
}
/**
* Block caller until {@link Channel} is connected and protocol is
* negotiated with handshake.
*/
protected void blockUntilConnected() {
for (;;) {
if (ConnectionState.DONE == state) {
throw new IllegalStateException("Client connection is done");
}
if (isReady()) {
return;
} else {
block();
}
}
}
@Override
public void close() {
closeChannel();
super.close();
}
private void block() {
LOG.trace("Waiting for connection to be established and negotiated...");
Thread.yield();
Uninterruptibles.sleepUninterruptibly(100, TimeUnit.MILLISECONDS);
}
@Override
public void awaitConnection() {
blockUntilConnected();
}
@Override
public String toString() {
return "AbstractReconnectingClient [state=" + state + ", uri=" + uri + ", sessionType=" + sessionType
+ ", hashCode=" + hashCode() + "]";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy