/*
* Copyright 2004 - 2008 Christian Sprajc. All rights reserved.
*
* This file is part of PowerFolder.
*
* PowerFolder is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation.
*
* PowerFolder is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PowerFolder. If not, see <http://www.gnu.org/licenses/>.
*
* $Id$
*/
package de.dal33t.powerfolder.net;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
import de.dal33t.powerfolder.ConfigurationEntry;
import de.dal33t.powerfolder.ConnectResult;
import de.dal33t.powerfolder.Constants;
import de.dal33t.powerfolder.Controller;
import de.dal33t.powerfolder.Member;
import de.dal33t.powerfolder.NetworkingMode;
import de.dal33t.powerfolder.PFComponent;
import de.dal33t.powerfolder.clientserver.ServerClient;
import de.dal33t.powerfolder.light.MemberInfo;
import de.dal33t.powerfolder.message.Identity;
import de.dal33t.powerfolder.util.Debug;
import de.dal33t.powerfolder.util.Reject;
import de.dal33t.powerfolder.util.compare.MemberComparator;
/**
* Responsible for reconnecting to remote nodes.
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc</a>
* @version $Revision: 1.5 $
*/
public class ReconnectManager extends PFComponent {
/** Queue holding all nodes, which are waiting to be reconnected */
private List<Member> reconnectionQueue;
/** The collection of reconnector */
private List<Reconnector> reconnectors;
// Counter for started reconntor, running number
private AtomicInteger reconnectorCounter = new AtomicInteger(0);
private boolean started;
public ReconnectManager(Controller controller) {
super(controller);
// Linkedlist, faster for queue useage
reconnectionQueue = new LinkedList<Member>();
// All reconnectors
reconnectors = Collections
.synchronizedList(new ArrayList<Reconnector>());
}
/**
* This starts the connecting process to other nodes, should be done, after
* UI is opend.
*/
public void start() {
started = true;
// Start reconnectors
logFine("Starting Reconnection Manager. Going to connect to other nodes now...");
buildReconnectionQueue();
// Resize reconnector pool from time to time. First execution: NOW
getController().getController().scheduleAndRepeat(
new ReconnectorPoolResizer(), 0,
Constants.RECONNECTOR_POOL_SIZE_RESIZE_TIME * 1000);
}
public void shutdown() {
started = false;
// Shutdown reconnectors
synchronized (reconnectors) {
logFine("Shutting down " + reconnectors.size() + " reconnectors");
for (Iterator<Reconnector> it = reconnectors.iterator(); it
.hasNext();)
{
Reconnector reconnector = it.next();
reconnector.shutdown();
it.remove();
}
}
synchronized (reconnectionQueue) {
reconnectionQueue.clear();
reconnectionQueue.notifyAll();
}
}
public boolean isStarted() {
return started;
}
/**
* @return the size of the reconnection queue
*/
public int countReconnectionQueue() {
return reconnectionQueue.size();
}
/**
* @return unmodifiable reference to the internal reconnection queue.
*/
public Collection<Member> getReconnectionQueue() {
return Collections.unmodifiableCollection(reconnectionQueue);
}
/**
* Marks a node for immediate reconnection. Actually puts it in front of
* reconnection queue and ensures, that is gets reconnected immediately.
*
* @param node
*/
public void markNodeForImmediateReconnection(Member node) {
if (!started) {
logFine("ReconnectManager not started. Unable to spawn new reconnector to "
+ node + ". Queue: " + reconnectionQueue);
return;
}
if (isFiner()) {
logFiner("Marking node for immediate reconnect: " + node);
}
if (node.isConnected() || node.isConnecting()) {
// Skip, not necessary.
return;
}
synchronized (reconnectionQueue) {
// Remove node
reconnectionQueue.remove(node);
// Add at start
reconnectionQueue.add(0, node);
reconnectionQueue.notify();
}
// Wait 20 ms to let one reconnector grab the node.
try {
Thread.sleep(20);
} catch (InterruptedException e) {
logFiner("InterruptedException", e);
return;
}
// None has take the node to reconnect.
// Spawn new reconnector
if (reconnectionQueue.contains(node)) {
if (isFine()) {
logFine("Spawning new Reconnector (" + (reconnectors.size() + 1)
+ " total) to get faster reconnect to " + node);
}
if (!started) {
logSevere("ReconnectManager not started. Unable to spawn new reconnector to "
+ node + ". Queue: " + reconnectionQueue);
return;
}
synchronized (reconnectors) {
Reconnector reconnector = new Reconnector();
// add reconnector to nodemanager
reconnectors.add(reconnector);
// and start
reconnector.start();
}
} else if (isFiner()) {
logFiner("Not required to spawn new reconnector to " + node
+ ". Queue: " + reconnectionQueue);
}
}
/**
* Checks if a reconnection to this node would be useful.
*
* @param node
* the node to connect to
* @return true if added to the reconnection queue, false if not.
*/
public boolean considerReconnectionTo(Member node) {
if (shouldBeAddedToReconQueue(node)) {
synchronized (reconnectionQueue) {
// Add node to reconnection queue
if (!reconnectionQueue.contains(node)) {
reconnectionQueue.add(node);
// Resort reconnection queue
Collections.sort(reconnectionQueue,
MemberComparator.BY_RECONNECTION_PRIORITY);
reconnectionQueue.notify();
return true;
}
}
}
return false;
}
/**
* Freshly refills the reconnection queue. The nodes contained are tried to
* reconnected. Also removes unused nodes
*/
public void buildReconnectionQueue() {
int nBefore = reconnectionQueue.size();
synchronized (reconnectionQueue) {
reconnectionQueue.clear();
for (Member node : getController().getNodeManager()
.getNodesAsCollection())
{
if (shouldBeAddedToReconQueue(node)) {
reconnectionQueue.add(node);
}
}
// Lately connect first
Collections.sort(reconnectionQueue,
MemberComparator.BY_RECONNECTION_PRIORITY);
if (isFiner()) {
logFiner("Freshly filled reconnection queue with "
+ reconnectionQueue.size() + " nodes, " + nBefore
+ " were in queue before");
}
if (getController().isVerbose()) {
Debug.writeNodeListCSV(reconnectionQueue,
"ReconnectionQueue.csv");
}
if (reconnectionQueue.size() > 100) {
logWarning("Reconnection queue contains more than 200 nodes");
}
// Notify threads
if (!reconnectionQueue.isEmpty()) {
reconnectionQueue.notify();
}
}
}
// Internal methods *******************************************************
private boolean shouldBeAddedToReconQueue(Member node) {
Reject.ifNull(node, "Node is null");
if (!started) {
return false;
}
if (node.getInfo().isInvalid(getController())) {
// Invalid
return false;
}
if (node.isConnected() || node.isMySelf()) {
// not process already connected nodes
return false;
}
if (node.isConnecting()) {
return false;
}
if (node.receivedWrongIdentity()) {
return false;
}
if (!node.isInteresting()) {
return false;
}
if (node.isServer() || ServerClient.isTempServerNode(node.getInfo())) {
// Server nodes get pushed into reconnection queue by own thread
return false;
}
if (getController().getIOProvider().getRelayedConnectionManager()
.isRelay(node))
{
// Relay nodes get reconnected by own thread
return false;
}
if (getController().getNetworkingMode().equals(
NetworkingMode.SERVERONLYMODE))
{
// Never connect this way to the server, only thru ServerClient.
return false;
}
if (!node.isOnSameNetwork()) {
// Don't try to connect to nodes on different network.
return false;
}
// Always add friends
if (node.isFriend()) {
// Always try to connect to friends
return true;
}
// Results in server shutdown by HETZNER!!
// if (node.isOnLAN()) {
// // Always try to connect to LAN users
// return true;
// }
// Disable, could cause #609
// if (node.isUnableToConnect()) {
// boolean causedByDupeConnection = node.getLastProblem() != null
// && node.getLastProblem().problemCode == Problem.DUPLICATE_CONNECTION;
//
// if (!causedByDupeConnection) {
// // Do not connect if not connection is possible
// // But RE-try if this was caused by a dupe connection.
// logFiner(
// "Not tring to connect because of unable to connect: "
// + node);
// return false;
// }
// }
// Offline limit time, all nodes before this time are not getting
// reconnected
Date offlineLimitTime = new Date(System.currentTimeMillis()
- Constants.MAX_NODE_OFFLINE_TIME);
// Check if node was offline too long
Date lastConnectTime = node.getLastNetworkConnectTime();
boolean offlineTooLong = true;
offlineTooLong = lastConnectTime != null ? lastConnectTime
.before(offlineLimitTime) : true;
if (offlineTooLong) {
return false;
}
if (node.isDontConnect()) {
// Don't connect if the node doesn't want to be connected!
return false;
}
int nConnectedSupernodes = getController().getNodeManager()
.countConnectedSupernodes();
if (node.isSupernode()
&& nConnectedSupernodes < Constants.N_SUPERNODES_TO_CONNECT)
{
// Connect to supernodes that are not offline too long
return true;
}
return false;
}
// Inner classes **********************************************************
/**
* Resizes the pool of active reconnectors
*/
private class ReconnectorPoolResizer extends TimerTask {
@Override
public void run() {
if (!started) {
return;
}
synchronized (reconnectors) {
// Remove dead reconnectors.
for (Iterator<Reconnector> it = reconnectors.iterator(); it
.hasNext();)
{
Reconnector reconnector = it.next();
if (!reconnector.isAlive() || reconnector.isInterrupted()) {
it.remove();
}
}
// Now do the actual resizing.
int nReconnector = reconnectors.size();
// Calculate required reconnectors. check min / max number.
int reqReconnectors = Math.max(
Constants.MIN_NUMBER_RECONNECTORS, Math.min(
Constants.MAX_NUMBER_RECONNECTORS, (reconnectionQueue
.size() / 3)));
int reconDiffer = reqReconnectors - nReconnector;
if (isFiner()) {
logFiner("Got " + reconnectionQueue.size()
+ " nodes queued for reconnection");
}
if (reconDiffer > 0) {
// We have to less reconnectors, spawning one...
for (int i = 0; i < reconDiffer; i++) {
final Reconnector reconnector = new Reconnector();
// add reconnector to nodemanager
reconnectors.add(reconnector);
// and start time shifted
getController().schedule(new Runnable() {
public void run() {
reconnector.start();
}
}, i * 500L);
}
logFine("Spawned " + reconDiffer + " reconnectors. "
+ reconnectors.size() + "/" + reqReconnectors
+ ", nodes in reconnection queue: "
+ reconnectionQueue.size());
} else if (reconDiffer < 0) {
logFine("Killing " + -reconDiffer
+ " Reconnectors. Currently have: " + nReconnector
+ " Reconnectors");
for (int i = 0; i < -reconDiffer; i++) {
// Kill one reconnector
if (reconnectors.size() <= 1) {
logWarning("Not killing last reconnector");
// Have at least one reconnector
break;
}
Reconnector reconnector = reconnectors.remove(0);
if (reconnector != null) {
logFiner("Killing reconnector " + reconnector);
reconnector.softShutdown();
}
}
}
}
}
}
/**
* Reconnector thread. Periodically tries to reconnect to nodes in the
* reconnection queue. Automatically starts new child reconnector if work is
* getting to hard (too much nodes)
*
* @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc </a>
*/
private class Reconnector extends Thread {
private boolean reconStarted;
private Member currentNode;
private Reconnector() {
super("Reconnector " + reconnectorCounter.addAndGet(1));
}
public void start() {
if (!started) {
throw new IllegalStateException(
"Unable to start reconnector. ReconnectManager not started");
}
super.start();
reconStarted = true;
}
/**
* soft shutdown. does not stop the current recon try.
*/
public void softShutdown() {
reconStarted = false;
reconnectorCounter.decrementAndGet();
}
public void shutdown() {
softShutdown();
interrupt();
if (currentNode != null) {
currentNode.shutdown();
}
}
private int getIdleWaitSeconds() {
return ConfigurationEntry.CONNECT_WAIT.getValueInt(getController());
}
public void run() {
if (isFiner()) {
logFiner("Starting reconnector: " + getName());
}
while (this.reconStarted) {
if (!started) {
logFine("Stopping " + this + ". ReconnectManager is down");
break;
}
if (!getController().getNodeManager().isStarted()) {
logFine("Stopping " + this + ". NodeManager is down");
break;
}
synchronized (reconnectionQueue) {
if (reconnectionQueue.isEmpty()) {
int idleSeconds = getIdleWaitSeconds();
logFine("Reconnection queue empty. " + this
+ " going on idle for " + idleSeconds + " seconds");
try {
reconnectionQueue.wait(1000L * idleSeconds);
} catch (InterruptedException e) {
logFiner(e);
break;
}
if (reconnectionQueue.isEmpty()) {
// Rebuilds reconnection queue if required
buildReconnectionQueue();
}
}
}
synchronized (reconnectionQueue) {
if (!reconnectionQueue.isEmpty()) {
// Take the first node out of the reconnection queue
currentNode = reconnectionQueue.remove(0);
if (currentNode.isConnected()
|| currentNode.isConnecting())
{
// Already reconnecting. Skip
if (isFiner()) {
logFiner("Not reconnecting to "
+ currentNode.getNick()
+ ", already reconnecting/connected");
}
currentNode = null;
}
}
if (currentNode == null) {
continue;
}
// MARK connecting ***
if (currentNode.markConnecting() >= 2) {
currentNode.unmarkConnecting();
if (isFine()) {
logFine("Skipping: " + currentNode);
}
continue;
} else {
if (isFiner()) {
logFiner("Picked node for reconnect: "
+ currentNode);
}
}
}
long start = System.currentTimeMillis();
try { // UNMARK connecting try/finally ***
// A node could be obtained from the reconnection queue, try
// to connect now
if (!ServerClient.isTempServerNode(currentNode.getInfo())) {
try {
// Reconnect, Don't mark connecting. already done.
ConnectResult res = currentNode.reconnect(false);
if (isFiner()) {
logFiner("Reconnect to " + currentNode + ": "
+ res);
}
} catch (InvalidIdentityException e) {
Identity otherNodeId = e.getFrom().getIdentity();
MemberInfo otherNodeInfo = otherNodeId != null
&& otherNodeId.getMemberInfo() != null
? otherNodeId.getMemberInfo()
: null;
if (otherNodeInfo != null
&& otherNodeInfo
.isOnSameNetwork(getController()))
{
Member otherNode = otherNodeInfo.getNode(
getController(), true);
boolean rec = considerReconnectionTo(otherNode);
logFine("Invalid identity from " + currentNode
+ ". Found: " + otherNode
+ ". Going to reconned it ? " + rec);
}
}
} else {
// Temporary server node, directly connect to
// IP/hostname
if (isFine()) {
logFine("Tring to connect to temporary server node at "
+ currentNode.getHostName()
+ ":"
+ currentNode.getPort()
+ ". ID: "
+ currentNode.getId());
}
try {
ConnectionHandler conHan = getController()
.getIOProvider().getConnectionHandlerFactory()
.tryToConnect(currentNode.getInfo());
getController().getNodeManager().acceptConnection(
conHan);
} catch (ConnectionException e1) {
logFiner("ConnectionException", e1);
}
}
} finally {
currentNode.unmarkConnecting();
}
}
}
public String toString() {
return getName();
}
}
}