/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.hadoop.hdfs.notifier;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hdfs.notifier.NamespaceNotifierClient.NotConnectedToServerException;
import org.apache.hadoop.hdfs.notifier.NamespaceNotifierClient.ServerAlreadyKnownException;
import org.apache.hadoop.hdfs.notifier.NamespaceNotifierClient.ServerNotKnownException;
import org.apache.hadoop.hdfs.notifier.EventType;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.protocol.TProtocolFactory;
import org.apache.thrift.server.TNonblockingServer;
import org.apache.thrift.server.TServer;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TNonblockingServerSocket;
import org.apache.thrift.transport.TNonblockingServerTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
import org.apache.thrift.transport.TTransportFactory;
import com.google.common.collect.Maps;
/**
* Used by the client to subscribe to the namespace notifier server.
*/
public class NamespaceNotifierClient implements Runnable {
public static final Log LOG = LogFactory.getLog(NamespaceNotifierClient.class);
// The object that will get our callbacks
Watcher watcher;
String listeningHost;
int listeningPort;
// All the watches that this client has placed.
// Mapping event to the last transaction id received
ConcurrentMap<NamespaceEventKey, Long> watchedEvents;
// When we should shutdown
volatile boolean shouldShutdown = false;
// The thrift handler implementation
ClientHandler.Iface handler;
TServer tserver;
ConnectionManager connectionManager;
Random generator;
/**
* Constructor used when the Namespace Notification Server is running just
* on one machine.
*
* @param watcher The notified component
* @param host The notification server hostname or IP address
* @param port The notification server listening port
* @param listeningPort the port on which this client should start the
* thrift service.
* @throws TException when failing to connect to the server.
*/
public NamespaceNotifierClient(Watcher watcher,
String host, int port, int listeningPort) throws TException {
this(watcher, Arrays.asList(host), port, listeningPort);
}
/**
* Constructor used when the Namespace Notification server is running on
* multiple machines, but on all machines it's listening on the same port
* number.
*
* @param watcher The notified component
* @param hosts The notification servers hostnames or IP addresses
* @param port The notification servers listening port
* @param listeningPort the port on which this client should start the
* thrift service.
* @throws TException when failing to connect to the server.
*/
public NamespaceNotifierClient(Watcher watcher,
List<String> hosts, int port, int listeningPort)
throws TException {
this(watcher, hosts, Arrays.asList(port), listeningPort);
}
/**
* Constructor used when the Namespace Notification server is running on
* multiple machines and it's not listening on the same port number on all
* machines.
*
* @param watcher The notified component
* @param hosts The notification servers hostnames or IP addresses
* @param ports The notification servers listening ports
* @param listeningPort the port on which this client should start the
* thrift service.
* @throws TException when failing to connect to the server.
*/
public NamespaceNotifierClient(Watcher watcher,
List<String> hosts, List<Integer> ports, int listeningPort)
throws TException {
this.watcher = watcher;
watchedEvents = new ConcurrentHashMap<NamespaceEventKey, Long>();
try {
listeningHost = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
throw new TException(e);
}
this.listeningPort = listeningPort;
handler = new ClientHandlerImpl(this);
// Ensure pseudo-random seed between clients
long seed = System.currentTimeMillis() + listeningPort +
listeningHost.hashCode();
if (LOG.isDebugEnabled()) {
LOG.debug(listeningPort + ": using seed " + seed);
}
generator = new Random(seed);
// Setup the Thrift server
TProtocolFactory protocolFactory = new TBinaryProtocol.Factory();
TTransportFactory transportFactory = new TFramedTransport.Factory();
TNonblockingServerTransport serverTransport;
ClientHandler.Processor<ClientHandler.Iface> processor =
new ClientHandler.Processor<ClientHandler.Iface>(handler);
serverTransport = new TNonblockingServerSocket(listeningPort);
TNonblockingServer.Args serverArgs =
new TNonblockingServer.Args(serverTransport);
serverArgs.processor(processor).transportFactory(transportFactory)
.protocolFactory(protocolFactory);
tserver = new TNonblockingServer(serverArgs);
connectionManager = new ConnectionManager(hosts, ports, this);
LOG.info(listeningPort + ": Successfully initialized namespace" +
" notifier client");
}
/**
* Called by the ConnectionManager when the connection state changed.
* The connection lock is hold when calling this method, so no
* other methods from the ConnectionManager should be called here.
*
* @param newState the new state
* @return when the new state is DISCONNECTED_HIDDEN or DISCONNECTED_VISIBLE,
* then the return value is ignored. If the new state is CONNECTED,
* then the return value shows if the NamespaceNotifierClient accepts
* or not the new server. If it isn't accepted, the ConnectionManager
* will try connecting to another server.
*/
boolean connectionStateChanged(int newState) {
switch (newState) {
case ConnectionManager.CONNECTED:
LOG.info(listeningPort + ": Switched to CONNECTED state.");
// Try to resubscribe all the watched events
try {
return resubscribe();
} catch (Exception e) {
LOG.error(listeningPort + ": Resubscribing failed", e);
return false;
}
case ConnectionManager.DISCONNECTED_VISIBLE:
LOG.info(listeningPort + ": Switched to DISCONNECTED_VISIBLE state");
for (NamespaceEventKey eventKey : watchedEvents.keySet())
watchedEvents.put(eventKey, -1L);
watcher.connectionFailed();
break;
case ConnectionManager.DISCONNECTED_HIDDEN:
LOG.info(listeningPort + ": Switched to DISCONNECTED_HIDDEN state.");
}
return true;
}
long getCurrentConnectionToken() {
return connectionManager.getConnectionToken();
}
/**
* Adds the specified server to the pool of known servers.
* @param host
* @param port
* @throws ServerAlreadyKnownException if the server is already in the pool
* of known servers.
*/
public void addServer(String host, int port)
throws ServerAlreadyKnownException {
connectionManager.addServer(host, port);
}
/**
* Removes the specified server from the pool of known servers.
* @param host
* @param port
* @throws ServerNotKnownException if the server isn't in the pool of
* known servers.
*/
public void removeServer(String host, int port)
throws ServerNotKnownException {
connectionManager.removeServer(host, port);
}
/**
* Sets the value of the timeout after which we will consider a server
* failed.
* @param timeout the value in milliseconds for the timeout after which
* we will consider the server failed. Defaults to 50000 (50 seconds).
*/
public void setServerTimeout(long timeout) {
connectionManager.setServerTimeout(timeout);
}
/**
* Sets the value of the time between consecutive connect retries. The
* connection is retried only after Watcher.connectionFailed was called.
* @param retryTime the retry time in milliseconds.
*/
public void setConnectRetryTime(long retryTime) {
connectionManager.setConnectRetryTime(retryTime);
}
/**
* The watcher (given in the constructor) will be notified when an
* event of the given type and at the given path will happen. He will
* keep receiving notifications until the watch is removed with
* {@link #removeWatch(String, EventType)}.
*
* The subscription is considered done if the method doesn't throw an
* exception (even if Watcher.connectionFailed is called before this
* method returns).
*
* @param path the path where the watch is placed. For the FILE_ADDED event
* type, this represents the path of the directory under which the
* file will be created.
* @param watchType the type of the event for which we want to receive the
* notifications.
* @param transactionId the transaction id of the last received notification.
* Notifications will from and excluding the notification with this
* transaction id. If this is -1, then all notifications that
* happened after this method returns will be received and some
* of the notifications between the time the method was called
* and the time the method returns may be received.
* @throws WatchAlreadyPlacedException if the watch already exists for this
* path and type.
* @throws NotConnectedToServerException when the Watcher.connectionSuccessful
* method was not called (the connection to the server isn't
* established yet) at start-up or after a Watcher.connectionFailed
* call. The Watcher.connectionFailed could of happened anytime
* since the last Watcher.connectionSuccessful call until this
* method returns.
* @throws TransactionIdTooOldException when the requested transaction id
* is too old and not loosing notifications can't be guaranteed.
* A solution would be a manual scanning and then calling the
* method again with -1 as the transactionId parameter.
*/
public void placeWatch(String path, EventType watchType,
long transactionId) throws TransactionIdTooOldException,
NotConnectedToServerException, InterruptedException,
WatchAlreadyPlacedException {
NamespaceEventKey eventKey = new NamespaceEventKey(path, watchType);
Object connectionLock = connectionManager.getConnectionLock();
LOG.info(listeningPort + ": Placing watch: " +
NotifierUtils.asString(eventKey) + " ...");
if (watchedEvents.containsKey(eventKey)) {
LOG.warn(listeningPort + ": Watch already exists at " +
NotifierUtils.asString(eventKey));
throw new WatchAlreadyPlacedException();
}
synchronized (connectionLock) {
connectionManager.waitForTransparentConnect();
if (!subscribe(path, watchType, transactionId)) {
connectionManager.failConnection(true);
connectionManager.waitForTransparentConnect();
if (!subscribe(path, watchType, transactionId)) {
// Since we are failing visible to the client, then there isn't
// a need to request from a given txId
watchedEvents.put(eventKey, -1L);
connectionManager.failConnection(false);
return;
}
}
watchedEvents.put(eventKey, transactionId);
}
}
/**
* Removes a previously placed watch for a particular event type from the
* given path. If the watch is not actually present at that path before
* calling the method, nothing will happen.
*
* To remove the watch for all event types at this path, use
* {@link #removeAllWatches(String)}.
*
* @param path the path from which the watch is removed. For the FILE_ADDED event
* type, this represents the path of the directory under which the
* file will be created.
* @param watchType the type of the event for which don't want to receive
* notifications from now on.
* @return true if successfully removed watch. false if the watch wasn't
* placed before calling this method.
* @throws WatchNotPlacedException if the watch wasn't placed before calling
* this method.
* @throws NotConnectedToServerException when the Watcher.connectionSuccessful
* method was not called (the connection to the server isn't
* established yet) at start-up or after a Watcher.connectionFailed
* call. The Watcher.connectionFailed could of happened anytime
* since the last Watcher.connectionSuccessfull call until this
* method returns.
*/
public void removeWatch(String path, EventType watchType)
throws NotConnectedToServerException, InterruptedException,
WatchNotPlacedException {
NamespaceEvent event = new NamespaceEvent(path, watchType.getByteValue());
NamespaceEventKey eventKey = new NamespaceEventKey(path, watchType);
Object connectionLock = connectionManager.getConnectionLock();
ServerHandler.Client server;
LOG.info(listeningPort + ": removeWatch: Removing watch from " +
NotifierUtils.asString(eventKey) + " ...");
if (!watchedEvents.containsKey(eventKey)) {
LOG.warn(listeningPort + ": removeWatch: watch doesen't exist at " +
NotifierUtils.asString(eventKey) + " ...");
throw new WatchNotPlacedException();
}
synchronized (connectionLock) {
connectionManager.waitForTransparentConnect();
server = connectionManager.getServer();
try {
server.unsubscribe(connectionManager.getId(), event);
} catch (InvalidClientIdException e1) {
LOG.warn(listeningPort + ": removeWatch: server deleted us", e1);
connectionManager.failConnection(true);
} catch (ClientNotSubscribedException e2) {
LOG.error(listeningPort + ": removeWatch: event not subscribed", e2);
} catch (TException e3) {
LOG.error(listeningPort + ": removeWatch: failed communicating to" +
" server", e3);
connectionManager.failConnection(true);
}
watchedEvents.remove(eventKey);
}
if (LOG.isDebugEnabled()) {
LOG.debug(listeningPort + ": Unsubscribed from " +
NotifierUtils.asString(eventKey));
}
}
/**
* Tests if a watch is placed at the given path and of the given type.
*
* @param path the path where we should test if a watch is placed. For the
* FILE_ADDED event type, this represents the path of the directory
* under which the file will be created.
* @param watchType the type of the event for which we test if a watch is
* present.
* @return <code>true</code> if a watch is placed, <code>false</code>
* otherwise.
*/
public boolean haveWatch(String path, EventType watchType) {
return watchedEvents.containsKey(new NamespaceEventKey(path, watchType));
}
/**
* @return true if notifications were received for all subscribed events,
* false otherwise.
*/
boolean receivedNotificationsForAllEvents() {
return !watchedEvents.values().contains(-1L);
}
/**
* Called right after a reconnect to resubscribe to all events. Must be
* called with the connection lock acquired.
*/
private boolean resubscribe() throws TransactionIdTooOldException,
InterruptedException {
for (NamespaceEventKey eventKey : watchedEvents.keySet()) {
NamespaceEvent event = eventKey.getEvent();
if (!subscribe(event.getPath(), EventType.fromByteValue(event.getType()),
watchedEvents.get(eventKey))) {
return false;
}
}
return true;
}
/**
* Should be called with the connection lock acquired and only in the
* <code>CONNECTED</code> state.
* @param path
* @param watchType
* @param transactionId
* @return true if connected, false otherwise.
* @throws TransactionIdTooOldException
*/
private boolean subscribe(String path, EventType watchType,
long transactionId)
throws TransactionIdTooOldException, InterruptedException {
ServerHandler.Client server = connectionManager.getServer();
NamespaceEvent event = new NamespaceEvent(path, watchType.getByteValue());
if (LOG.isDebugEnabled()) {
LOG.debug(listeningPort + ": subscribe: Trying to subscribe for " +
NotifierUtils.asString(event) + " ... from txId " + transactionId);
}
for (int retries = 0; retries < 3; retries ++) {
try {
server.subscribe(connectionManager.getId(), event, transactionId);
if (LOG.isDebugEnabled()) {
LOG.debug(listeningPort + ": subscribe: successful");
}
return true;
} catch (TransactionIdTooOldException e) {
LOG.warn(listeningPort + ": Failed to subscribe [1]", e);
throw e;
} catch (InvalidClientIdException e) {
LOG.warn(listeningPort + ": Failed to subscribe [2]", e);
} catch (TException e) {
LOG.warn(listeningPort + ": Failed to subscribe [3]", e);
Thread.sleep(1000);
}
}
return false;
}
@Override
public void run() {
LOG.info(listeningPort + ": Running ...");
new Thread(connectionManager).start();
LOG.info(listeningPort + ": Starting thrift server on port " + listeningPort);
tserver.serve();
}
public void shutdown() {
shouldShutdown = true;
ServerHandler.Client server = connectionManager.getServer();
connectionManager.shutdown();
try {
server.unregisterClient(connectionManager.getId());
} catch (InvalidClientIdException e1) {
LOG.warn(listeningPort + ": Server deleted us before shutdown", e1);
} catch (TException e2) {
LOG.warn(listeningPort + ": Failed to unregister client gracefully", e2);
}
tserver.stop();
}
/**
* Raised when the client tries server related operations, but the
* Watcher.connectionSuccessful method was not called.
*/
static public class NotConnectedToServerException extends Exception {
private static final long serialVersionUID = 1L;
public NotConnectedToServerException(String arg) {
super(arg);
}
public NotConnectedToServerException() {
super();
}
}
/**
* Called when the placeWatch method is called, but the watch is already
* present.
*/
static public class WatchAlreadyPlacedException extends Exception {
private static final long serialVersionUID = 1L;
public WatchAlreadyPlacedException(String arg) {
super(arg);
}
public WatchAlreadyPlacedException() {
super();
}
}
/**
* Called when the removeWatch method is called, but the watch wasn't placed.
*/
static public class WatchNotPlacedException extends Exception {
private static final long serialVersionUID = 1L;
public WatchNotPlacedException(String arg) {
super(arg);
}
public WatchNotPlacedException() {
super();
}
}
/**
* Called when the addServer method tries to add a server which is already
* stored in the internal data structures.
*/
static public class ServerAlreadyKnownException extends Exception {
private static final long serialVersionUID = 1L;
public ServerAlreadyKnownException(String arg) {
super(arg);
}
public ServerAlreadyKnownException() {
super();
}
}
/**
* Called when the removeServer method tries to remove a server which is not
* stored in the internal data structures.
*/
static public class ServerNotKnownException extends Exception {
private static final long serialVersionUID = 1L;
public ServerNotKnownException(String arg) {
super(arg);
}
public ServerNotKnownException() {
super();
}
}
}
class ConnectionManager implements Runnable {
public static final Log LOG = LogFactory.getLog(NamespaceNotifierClient.class);
static final int SOCKET_TIMEOUT = 35000;
static final long DEFAULT_SERVER_TIMEOUT = 50000;
static final int DEFAULT_CONNECT_RETRY_TIME = 1000;
public static final int CONNECTED = 0;
public static final int DISCONNECTED_HIDDEN = 1;
public static final int DISCONNECTED_VISIBLE = 2;
private int state = DISCONNECTED_VISIBLE;
int listeningPort;
// This lock is hold when doing operations that may modify the
// connection state
private Object connectionLock = new Object();
// Used to wait and notify of when we should start retrying the connection
// with the server.
private Object retryConnectionCondition = new Object();
// Connection to notification servers information
private List<Map.Entry<String, Integer>> servers;
// The server thrift object (if we are connected to a server)
private ServerHandler.Client server = null;
private long connectionToken;
// The id of the server we are currently connected to
volatile String serverId;
// The id currently assigned to us by the server we are connected to.
volatile long id;
private volatile long serverTimeout = DEFAULT_SERVER_TIMEOUT;
private volatile int connectRetryTime = DEFAULT_CONNECT_RETRY_TIME;
ServerTracker tracker;
NamespaceNotifierClient notifierClient;
public ConnectionManager(List<String> hosts, List<Integer> ports,
NamespaceNotifierClient notifierClient) {
serverId = null;
id = -1;
tracker = new ServerTracker();
this.notifierClient = notifierClient;
this.listeningPort = notifierClient.listeningPort;
servers = new ArrayList<Map.Entry<String,Integer>>();
for (int i = 0; i < hosts.size(); i ++) {
String host = hosts.get(i);
int port = ports.get(ports.size() == 0 ? 0 : i);
servers.add(Maps.immutableEntry(host, port));
}
// So clients try have different priority for servers, avoiding
// all the clients connecting to one server.
Collections.shuffle(servers, notifierClient.generator);
}
private int getServerPosition(String host, int port) {
for (int i = 0; i < servers.size(); i ++) {
Map.Entry<String, Integer> serverEntry = servers.get(i);
if (serverEntry.getKey().equals(host) && serverEntry.getValue() == port) {
return i;
}
}
return -1;
}
void addServer(String host, int port)
throws ServerAlreadyKnownException {
if (getServerPosition(host, port) != -1) {
throw new ServerAlreadyKnownException("Already got " + host + ":" +
port);
}
// Put in a random position to ensure load balancing across servers
int position = notifierClient.generator.nextInt(servers.size() + 1);
servers.add(position, Maps.immutableEntry(host, port));
}
void removeServer(String host, int port)
throws ServerNotKnownException {
int position = getServerPosition(host, port);
if (position == -1) {
throw new ServerNotKnownException("Unknown host " + host + ":" + port);
}
servers.remove(position);
}
void setServerTimeout(long timeout) {
serverTimeout = timeout;
}
void setConnectRetryTime(long retryTime) {
setConnectRetryTime(retryTime);
}
long getId() {
return id;
}
String getServerId() {
return serverId;
}
ServerHandler.Client getServer() {
return server;
}
/**
* Gets the current connection state.
* @param connectionLockHold if the connection lock returned by
* getConnectionLock is being hold at the moment within
* a synchronized block.
* @return the current state (CONNECTED, DISCONNECTED_HIDDEN or
* DISCONNECTED_VISIBLE).
*/
int getConnectionState(boolean connectionLockHold) {
if (connectionLockHold) {
return state;
}
synchronized (connectionLock) {
return state;
}
}
/**
* @return The most recently generated connection token.
*/
long getConnectionToken() {
return connectionToken;
}
/**
* @return An object that is being hold with synchronized() when doing
* operations that may change the connection state. Holding
* this object thus guarantees that no connection state changes
* will occur while doing so.
*/
Object getConnectionLock() {
return connectionLock;
}
/**
* Must be called holding the connection lock returned by getConnectionLock.
* It waits until the current connection state is CONNECTED. If it ever
* gets to DISCONNECTED_VISIBLE it will raise an exception. If the current
* state is CONNECTED, then it will return without waiting.
*
* @throws InterruptedException
* @throws NotConnectedToServerException when we got into a
* DISCONNECTED_VISIBLE state.
*/
void waitForTransparentConnect() throws InterruptedException,
NotConnectedToServerException {
if (state == DISCONNECTED_VISIBLE) {
LOG.warn(listeningPort + ": waitForTransparentConnect: got visible" +
" disconnected state");
throw new NotConnectedToServerException();
}
// Wait until we are not hidden disconnected
while (state != CONNECTED) {
connectionLock.wait();
switch (state) {
case CONNECTED:
break;
case DISCONNECTED_HIDDEN:
continue;
case DISCONNECTED_VISIBLE:
LOG.warn(listeningPort + ": waitForTransparentConnect: got visible" +
" disconnected state");
throw new NotConnectedToServerException();
}
}
}
private boolean connect() {
LOG.info(listeningPort + ": Connecting ...");
synchronized (connectionLock) {
// Ensure there are no potential lost messages.
if (state == DISCONNECTED_HIDDEN &&
!notifierClient.receivedNotificationsForAllEvents()) {
LOG.info(listeningPort + ": Didn't received notifications for" +
" all events");
failConnection(false);
return false;
} else {
LOG.info(listeningPort + ": Received notifications for all events.");
}
for (Map.Entry<String, Integer> serverAddr : servers) {
String host = serverAddr.getKey();
int port = serverAddr.getValue();
LOG.info(listeningPort + ": Trying to connect to " + host + ":" +
port);
ServerHandler.Client serverObj;
try {
serverObj = getServerConnection(host, port);
} catch (Exception e) {
LOG.error(listeningPort + ": Failed to connect to server at " +
host + ":" + port, e);
continue;
}
// The server must answer with this token
connectionToken = notifierClient.generator.nextLong();
LOG.info(listeningPort + ": Generated token: " + connectionToken);
try {
// Before this function returns, the server should make the
// registerServer call which if he answers with the correct token,
// it will set the serverId to his.
LOG.info(listeningPort + ": calling registerClient");
serverObj.registerClient(notifierClient.listeningHost,
notifierClient.listeningPort, connectionToken);
LOG.info(listeningPort + ": registerClient call successful");
} catch (RampUpException e1) {
LOG.info(listeningPort + ": Server " + host + ":" + port +
" in ramp up phase");
continue;
} catch (ClientConnectionException e2) {
LOG.error(listeningPort + ": The server failed to connect to us", e2);
continue;
} catch (Exception e) {
LOG.error(listeningPort + ": Server " + host + ":" + port +
" communication failure", e);
continue;
}
if (serverId == null || serverId.isEmpty()) {
LOG.info(listeningPort + ": The server answered with a bad token." +
" trying next server ...");
continue;
}
LOG.info(listeningPort + ": The server answered with correct token.");
server = serverObj;
state = CONNECTED;
if (!notifierClient.connectionStateChanged(state)) {
try {
server.unregisterClient(id);
} catch (Exception e) {}
failConnection(false);
return false;
}
tracker.messageReceived();
LOG.info(listeningPort + ": Connection status: SUCCESS");
return true;
}
}
LOG.info(listeningPort + ": Connection status: FAILED");
return false;
}
void failConnection(boolean hiddenToClient) {
LOG.info(listeningPort + ": Failing connection. Hidden to client=" +
hiddenToClient);
serverId = null;
id = -1;
server = null;
if (hiddenToClient) {
state = DISCONNECTED_HIDDEN;
} else {
state = DISCONNECTED_VISIBLE;
}
notifierClient.connectionStateChanged(state);
synchronized (retryConnectionCondition) {
retryConnectionCondition.notify();
}
}
@Override
public void run() {
// Initial connect
forceConnect();
new Thread(tracker).start();
// Retry on failure
while (!notifierClient.shouldShutdown) {
synchronized (retryConnectionCondition) {
try {
retryConnectionCondition.wait();
} catch (InterruptedException e) {
if (notifierClient.shouldShutdown) {
break;
}
continue;
}
}
if (getConnectionState(false) == DISCONNECTED_VISIBLE) {
notifierClient.watcher.connectionFailed();
}
if (notifierClient.shouldShutdown) {
break;
}
forceConnect();
}
}
void shutdown() {
synchronized (retryConnectionCondition) {
retryConnectionCondition.notify();
}
}
private void forceConnect() {
LOG.info(listeningPort + ": ConnectionChecker forcing connect ...");
while (true) {
LOG.info(listeningPort + ": forceConnect loop start ...");
try {
Thread.sleep(connectRetryTime);
} catch (InterruptedException e) {}
LOG.info(listeningPort + ": forceConnect trying connect ...");
int prevState = getConnectionState(false);
if (connect()) {
if (prevState == DISCONNECTED_VISIBLE) {
notifierClient.watcher.connectionSuccesful();
}
break;
}
LOG.info(listeningPort + ": forceConnect done trying connect");
}
LOG.info(listeningPort + ": forceConnect done");
}
private ServerHandler.Client getServerConnection(String host, int port)
throws TTransportException, IOException {
TTransport transport;
TProtocol protocol;
ServerHandler.Client serverObj;
transport = new TFramedTransport(new TSocket(host, port, SOCKET_TIMEOUT));
protocol = new TBinaryProtocol(transport);
serverObj = new ServerHandler.Client(protocol);
transport.open();
return serverObj;
}
class ServerTracker implements Runnable {
volatile long lastReceivedTimestamp = -1;
/**
* Should be called when a message was received from the server.
*/
public void messageReceived() {
lastReceivedTimestamp = System.currentTimeMillis();
}
@Override
public void run() {
lastReceivedTimestamp = System.currentTimeMillis();
while (!notifierClient.shouldShutdown) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
synchronized (connectionLock) {
if (state != CONNECTED) {
continue;
}
if (System.currentTimeMillis() > lastReceivedTimestamp + serverTimeout) {
LOG.info(listeningPort + ": ServerTracker: Server timeout." +
" Failing connection ...");
failConnection(true);
}
}
}
}
}
}