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

convex.peer.ConnectionManager Maven / Gradle / Ivy

The newest version!
package convex.peer;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.UnresolvedAddressException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import convex.api.Convex;
import convex.core.Belief;
import convex.core.Constants;
import convex.core.Peer;
import convex.core.Result;
import convex.core.State;
import convex.core.data.ACell;
import convex.core.data.AString;
import convex.core.data.AVector;
import convex.core.data.AccountKey;
import convex.core.data.Hash;
import convex.core.data.Keywords;
import convex.core.data.PeerStatus;
import convex.core.data.SignedData;
import convex.core.data.Vectors;
import convex.core.exceptions.BadFormatException;
import convex.core.exceptions.MissingDataException;
import convex.core.lang.RT;
import convex.core.util.LoadMonitor;
import convex.core.util.Utils;
import convex.net.ChallengeRequest;
import convex.net.Connection;
import convex.net.Message;

/**
 * Class for managing the outbound Peer connections from a Peer Server.
 *
 * Outbound connections need special handling: - Should be trusted connections
 * to known peers - Should be targets for broadcast of belief updates - Should
 * be limited in number
 */
public class ConnectionManager extends AThreadedComponent {

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

	/**
	 * Pause for each iteration of Server connection loop.
	 */
	static final long SERVER_CONNECTION_PAUSE = 500;

	/**
	 * Default pause for each iteration of Server connection loop.
	 */
	static final long SERVER_POLL_DELAY = 2000;

	/**
	 * How long to wait for a belief poll request of status.
	 */
	static final long POLL_TIMEOUT_MILLIS = 2000;

	/**
	 * How long to wait for a complete acquire of a belief.
	 */
	static final long POLL_ACQUIRE_TIMEOUT_MILLIS = 12000;

	/**
	 * Timeout for a regular Belief delta broadcast
	 */
	private static final long BROADCAST_TIMEOUT = 1000;

	/**
	 * Map of current connections.
	 */
	private final HashMap connections = new HashMap<>();

	/**
	 * The list of outgoing challenges that are being made to remote peers
	 */
	private HashMap challengeList = new HashMap<>();

	private SecureRandom random = new SecureRandom();

	private long pollDelay;

	/**
	 * Celled by the connection manager to ensure we are tracking latest Beliefs on the network
	 * @throws InterruptedException 
	 */
	private void pollBelief() throws InterruptedException {
		try {
			// Poll only if no recent consensus updates
			long lastConsensus = server.getPeer().getConsensusState().getTimestamp().longValue();
			if (lastConsensus + pollDelay >= Utils.getCurrentTimestamp()) return;

			ArrayList conns = new ArrayList<>(connections.values());
			if (conns.size() == 0) {
				// Nothing to do
				// log.debug("No connections available to poll!");
				return;
			}
			
			// TODO: probably shouldn't make a new connection?
			// Maybe use Convex instance instead of Connection?
			Connection c = conns.get(random.nextInt(conns.size()));

			if (c.isClosed()) return;
			Convex convex = Convex.connect(c.getRemoteAddress());
			try {
				// use requestStatusSync to auto acquire hash of the status instead of the value
				Result result=convex.requestStatusSync(POLL_TIMEOUT_MILLIS);
				AVector status = result.getValue();

				Hash h=RT.ensureHash(status.get(0));

				Belief sb=(Belief) convex.acquire(h).get(POLL_ACQUIRE_TIMEOUT_MILLIS,TimeUnit.MILLISECONDS);

				server.queueBelief(Message.createBelief(sb));
			} finally {
				convex.close();
			}
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		} catch (RuntimeException | TimeoutException | ExecutionException | IOException t) {
			if (server.isLive()) {
				log.warn("Belief Polling failed: {}",t.getClass().toString()+" : "+t.getMessage());
			}
		}
	}

	private long lastConnectionUpdate=Utils.getCurrentTimestamp();
	
	protected void maintainConnections() throws InterruptedException {
		State s=server.getPeer().getConsensusState();

		long now=Utils.getCurrentTimestamp();
		long millisSinceLastUpdate=Math.max(0,now-lastConnectionUpdate);

		int targetPeerCount=getTargetPeerCount();
		int currentPeerCount=connections.size();
		double totalStake=s.computeStakes().get(null);

		AccountKey[] peers = connections.keySet().toArray(new AccountKey[currentPeerCount]);
		for (AccountKey p: peers) {
			Connection conn=connections.get(p);

			// Remove closed connections. No point keeping these
			if ((conn==null)||(conn.isClosed())) {
				closeConnection(p);
				currentPeerCount--;
				continue;
			}

			/*
			 *  Always remove Peers not staked in consensus. This should eliminate Peers that have
			 *  withdrawn, have trivial stake or are slashed from current consideration.
			 */
			PeerStatus ps=s.getPeer(p);
			if ((ps==null)||(ps.getTotalStake()<=Constants.MINIMUM_EFFECTIVE_STAKE)) {
				closeConnection(p);
				currentPeerCount--;
				continue;
			}

			/* Drop Peers randomly if they have a small stake
			 * This ensure that new peers will get picked up occasionally and
			 * the distribution of peers tends towards the level of stake over time
			 */
			if ((millisSinceLastUpdate>0)&&(currentPeerCount>=targetPeerCount)) {
				double prop=ps.getTotalStake()/totalStake; // proportion of stake represented by this Peer
				// Very low chance of dropping a Peer with high stake (more than
				double keepChance=Math.min(1.0, prop*targetPeerCount);

				if (keepChance<1.0) {

					double dropRate=millisSinceLastUpdate/(double)Config.PEER_CONNECTION_DROP_TIME;
					if (random.nextDouble()<(dropRate*(1.0-keepChance))) {
						closeConnection(p);
						currentPeerCount--;
						continue;
					}
				}
			}

			// send request for a trusted peer connection if necessary
			// TODO: need to find out why the response message is not being received by the peers
			requestChallenge(p, conn, server.getPeer());
		}

		// refresh peers list
		currentPeerCount=connections.size();
		if (currentPeerCount potentialPeers=s.getPeers().keySet();
		InetSocketAddress target=null;
		double accStake=0.0;
		for (ACell c:potentialPeers) {
			AccountKey peerKey=RT.ensureAccountKey(c);
			if (connections.containsKey(peerKey)) continue; // skip if already connected

			if (server.getPeerKey().equals(peerKey)) continue; // don't connect to self!!

			PeerStatus ps=s.getPeers().get(peerKey);
			if (ps==null) continue; // skip
			AString hostName=ps.getHostname();
			if (hostName==null) continue;
			InetSocketAddress maybeAddress=Utils.toInetSocketAddress(hostName.toString());
			if (maybeAddress==null) continue;
			long peerStake=ps.getPeerStake();
			if (peerStake>Constants.MINIMUM_EFFECTIVE_STAKE) {
				double t=random.nextDouble()*(accStake+peerStake);
				if (t>=accStake) {
					target=maybeAddress;
				}
				accStake+=peerStake;
			}
		}

		if (target!=null) {
			// Try to connect to Peer. If it fails, no worry, will retry another peer next time
			boolean success=connectToPeer(target) != null;
			if (!success) {
				log.warn("Failed to connect to Peer at "+target);
			}
		}
	}

	/**
	 * Gets the desired number of outgoing connections
	 * @return
	 */
	private int getTargetPeerCount() {
		Integer target;
		try {
			target = Utils.toInt(server.getConfig().get(Keywords.OUTGOING_CONNECTIONS));
		} catch (IllegalArgumentException ex) {
			target=null;
		}
		if (target==null) target=Config.DEFAULT_OUTGOING_CONNECTION_COUNT;
		return target;
	}


	public ConnectionManager(Server server) {
		super(server);

	}

	/**
	 * Close and remove a connection
	 *
	 * @param peerKey Peer key linked to the connection to close and remove.
	 *
	 */
	public synchronized void closeConnection(AccountKey peerKey) {
		if (connections.containsKey(peerKey)) {
			Connection conn=connections.get(peerKey);
			if (conn!=null) {
				conn.close();
			}
			connections.remove(peerKey);
		}
	}

	/**
	 * Close all outgoing connections from this Peer
	 */
	public synchronized void closeAllConnections() {
		for (Connection conn:connections.values()) {
			if (conn!=null) conn.close();
		}
		connections.clear();
	}

	/**
	 * Gets the current set of outbound peer connections from this server
	 *
	 * @return Set of connections
	 */
	public HashMap getConnections() {
		return connections;
	}

	/**
	 * Return true if a specified Peer is connected
	 * @param peerKey Public Key of Peer
	 * @return True if connected
	 *
	 */
	public boolean isConnected(AccountKey peerKey) {
		return connections.containsKey(peerKey);
	}


	/**
	 * Gets a connection based on the peers public key
	 * @param peerKey Public key of Peer
	 *
	 * @return Connection instance, or null if not found
	 */
	public Connection getConnection(AccountKey peerKey) {
		if (!connections.containsKey(peerKey)) return null;
		return connections.get(peerKey);
	}

	/**
	 * Returns the number of active connections
	 * @return Number of connections
	 */
	public int getConnectionCount() {
		return connections.size();
	}

	/**
	 * Returns the number of trusted connections
	 * @return Number of trusted connections
	 *
	 */
	public int getTrustedConnectionCount() {
		int result = 0;
		for (Connection connection : connections.values()) {
			if (connection.isTrusted()) {
				result ++;
			}
		}
		return result;
	}

	public void processChallenge(Message m, Peer thisPeer) {
		SignedData> signedData=null;
		try {
			signedData = m.getPayload();
			if ( signedData == null) {
				throw new BadFormatException("null challenge?");
			}
		} catch (BadFormatException e) {
			alertBadMessage(m,"Bad format in challenge");
			return;
		}
		
		AVector challengeValues = signedData.getValue();

		if (challengeValues == null || challengeValues.size() != 3) {
			log.debug("challenge data incorrect number of items should be 3 not ",RT.count(challengeValues));
			return;
		}
		
		// log.log(LEVEL_CHALLENGE_RESPONSE, "Processing challenge request from: " + pc.getRemoteAddress());

		// get the token to respond with
		Hash token = RT.ensureHash(challengeValues.get(0));
		if (token == null) {
			log.warn( "no challenge token provided");
			return;
		}

		// check to see if we are both want to connect to the same network
		Hash networkId = RT.ensureHash(challengeValues.get(1));
		if (networkId == null) {
			log.warn( "challenge data has no networkId");
			return;
		}
		if ( !networkId.equals(thisPeer.getNetworkID())) {
			log.warn( "challenge data has incorrect networkId");
			return;
		}
		// check to see if the challenge is for this peer
		AccountKey toPeer = RT.ensureAccountKey(challengeValues.get(2));
		if (toPeer == null) {
			log.warn( "challenge data has no toPeer address");
			return;
		}
		if ( !toPeer.equals(thisPeer.getPeerKey())) {
			log.warn( "challenge data has incorrect addressed peer");
			return;
		}

		// get who sent this challenge
		AccountKey fromPeer = signedData.getAccountKey();

		// send the signed response back
		AVector responseValues = Vectors.of(token, thisPeer.getNetworkID(), fromPeer, signedData.getHash());

		SignedData response = thisPeer.sign(responseValues);
		// log.log(LEVEL_CHALLENGE_RESPONSE, "Sending response to "+ pc.getRemoteAddress());
		Message resp=Message.createResponse(response);
		m.returnMessage(resp);
	}

	/**
	 * Processes a response message received from a peer
	 * @param m
	 * @param thisPeer
	 * @return
	 */
	AccountKey processResponse(Message m, Peer thisPeer) {
		SignedData signedData;
		try {
			signedData = m.getPayload();
		} catch (BadFormatException e) {
			return null;
		}

		log.debug( "Processing response request");

		@SuppressWarnings("unchecked")
		AVector responseValues = (AVector) signedData.getValue();

		if (responseValues.size() != 4) {
			log.warn( "response data incorrect number of items should be 4 not {}",responseValues.size());
			return null;
		}


		// get the signed token
		Hash token = RT.ensureHash(responseValues.get(0));
		if (token == null) {
			log.warn( "no response token provided");
			return null;
		}

		// check to see if we are both want to connect to the same network
		Hash networkId = RT.ensureHash(responseValues.get(1));
		if ( networkId == null || !networkId.equals(thisPeer.getNetworkID())) {
			log.warn( "response data has incorrect networkId");
			return null;
		}
		// check to see if the challenge is for this peer
		AccountKey toPeer = RT.ensureAccountKey(responseValues.get(2));
		if ( toPeer == null || !toPeer.equals(thisPeer.getPeerKey())) {
			log.warn( "response data has incorrect addressed peer");
			return null;
		}

		// hash sent by the response
		Hash challengeHash = RT.ensureHash(responseValues.get(3));

		// get who sent this challenge
		AccountKey fromPeer = signedData.getAccountKey();


		if ( !challengeList.containsKey(fromPeer)) {
			log.warn( "response from an unkown challenge");
			return null;
		}
		
		synchronized(challengeList) {

			// get the challenge data we sent out for this peer
			ChallengeRequest challengeRequest = challengeList.get(fromPeer);

			Hash challengeToken = challengeRequest.getToken();
			if (!challengeToken.equals(token)) {
				log.warn( "invalid response token sent");
				return null;
			}

			AccountKey challengeFromPeer = challengeRequest.getPeerKey();
			if (!signedData.getAccountKey().equals(challengeFromPeer)) {
				log.warn("response key does not match requested key, sent from a different peer");
				return null;
			}

			// hash sent by this peer for the challenge
			Hash challengeSourceHash = challengeRequest.getSendHash();
			if ( !challengeHash.equals(challengeSourceHash)) {
				log.warn("response hash of the challenge does not match");
				return null;
			}
			// remove from list incase this fails, we can generate another challenge
			challengeList.remove(fromPeer);

			Connection connection = getConnection(fromPeer);
			if (connection != null) {
				connection.setTrustedPeerKey(fromPeer);
			}

			// return the trusted peer key
			return fromPeer;
		}
	}



	/**
	 * Sends out a challenge to a connection that is not trusted.
	 * @param toPeerKey Peer key that we need to send the challenge too.
	 * @param connection untrusted connection
	 * @param thisPeer Source peer that the challenge is issued from
	 *
	 */
	public void requestChallenge(AccountKey toPeerKey, Connection connection, Peer thisPeer) {
		synchronized(challengeList) {
			if (connection.isTrusted()) {
				return;
			}
			// skip if a challenge is already being sent
			if (challengeList.containsKey(toPeerKey)) {
				if (!challengeList.get(toPeerKey).isTimedout()) {
					// not timed out, then continue to wait
					return;
				}
				// remove the old timed out request
				challengeList.remove(toPeerKey);
			}
			ChallengeRequest request = ChallengeRequest.create(toPeerKey);
			if (request.send(connection, thisPeer)>=0) {
				challengeList.put(toPeerKey, request);
			} else {
				// TODO: check OK to do nothing and send later?
			}
		}
	}

	/**
	 * Broadcasts a Message to all connected Peers
	 * 
	 * @param msg Message to broadcast
	 * @throws InterruptedException If broadcast is interrupted
	 *
	 */
	public synchronized void broadcast(Message msg) throws InterruptedException {
		HashMap hm=getCurrentConnections();
		
		long start=Utils.getCurrentTimestamp();
		while ((!hm.isEmpty())&&(start+BROADCAST_TIMEOUT>Utils.getCurrentTimestamp())) {
			ArrayList> left=new ArrayList<>(hm.entrySet());
			
			// Shuffle order for sending
			Utils.shuffle(left);
			
			for (Map.Entry me: left) {
				Connection pc=me.getValue();
				boolean sent;
				try {
					sent = pc.sendMessage(msg);
				} catch (IOException e) {
					// Something went wrong, lose this connection
					closeConnection(me.getKey());
					sent=false;
				}
				if (sent) {
					hm.remove(me.getKey());	
				} else {
					// log.warn("Broadcast failed!");
				}
			}
			
			// terminate loop if everything is successfully sent
			if (hm.isEmpty()) break;
			
			// Avoid a busy wait if buffers are full and still have things to send		
			LoadMonitor.down();
			Thread.sleep(10);
			LoadMonitor.up();
		}
		
		if ((!hm.isEmpty())&&server.isLive()) {
			ArrayList> left=new ArrayList<>(hm.entrySet());
			Map.Entry drop=left.get(random.nextInt(left.size()));
			AccountKey dropKey=drop.getKey();
			closeConnection(dropKey);
			log.warn("Unable to send broadcast to "+hm.size()+" peers, dropped one connection to: "+dropKey);
		}
	}

	private HashMap getCurrentConnections() {
		synchronized(connections) {
			return new HashMap<>(connections);
		}
	}

	/**
	 * Connects explicitly to a Peer at the given host address
	 * @param hostAddress Address to connect to
	 * @return new Connection, or null if attempt fails
	 * @throws InterruptedException 
	 */
	public Connection connectToPeer(InetSocketAddress hostAddress) throws InterruptedException {
		Connection newConn = null;
		try {
			// Use temp client connection to query status
			Convex convex=Convex.connect(hostAddress);
			Result result = convex.requestStatusSync(Config.DEFAULT_CLIENT_TIMEOUT);
			AVector status = result.getValue();
			// close the temp connection to Convex API
			convex.close();
			
			if (status == null || status.count()!=Config.STATUS_COUNT) {
				log.info("Bad status message from remote Peer");
				return null;
			}

			AccountKey peerKey =RT.ensureAccountKey(status.get(3));
			if (peerKey==null) return null;

			Connection existing=connections.get(peerKey);
			if ((existing!=null)&&!existing.isClosed()) return existing;
			synchronized(connections) {
				// reopen with connection to the peer and handle server messages
				newConn = Connection.connect(hostAddress, server.receiveAction, server.getStore(), null,Config.SOCKET_PEER_BUFFER_SIZE,Config.SOCKET_PEER_BUFFER_SIZE);
				connections.put(peerKey, newConn);
			}
		} catch (IOException | TimeoutException e) {
			// ignore any errors from the peer connections
			return null;
		} catch (UnresolvedAddressException e) {
			log.info("Unable to resolve host address: "+hostAddress);
			return null;
		}
		return newConn;
	}

	@Override
	public void close() {
		// broadcast GOODBYE message to all outgoing remote peers
		try {
			Message msg = Message.createGoodBye();
			broadcast(msg);
		} catch (InterruptedException e ) {
			// maintain interrupt status
			Thread.currentThread().interrupt();
		}
		
		super.close();
	}
	
	@Override
	public void start() {
		Object _pollDelay = server.getConfig().get(Keywords.POLL_DELAY);
		this.pollDelay = (_pollDelay == null) ? ConnectionManager.SERVER_POLL_DELAY : Utils.toInt(_pollDelay);

		super.start();
	}

	@Override
	protected void loop() throws InterruptedException {
		LoadMonitor.down();
		Thread.sleep(ConnectionManager.SERVER_CONNECTION_PAUSE);
		LoadMonitor.up();
		maintainConnections();
		pollBelief();
	}

	@Override
	protected String getThreadName() {
		return "Connection Manager thread at "+server.getPort();
	}

	/**
	 * Called to signal a bad / corrupt message from a Peer.
	 * @param m Message of concern 
	 * @param reason Reason message considered bad
	 */
	public void alertBadMessage(Message m, String reason) {
		// TODO Possibly dump Peer? Send a result indicating bad message?
		log.warn(reason);
	}

	/**
	 * Called to signal missing data in a Belief / Order
	 * @param m Message which caused alert
	 * @param e Missing data exception encountered
	 * @param peerKey Peer key which triggered missing data
	 */
	public void alertMissing(Message m, MissingDataException e, AccountKey peerKey) {
		if (log.isDebugEnabled()) {
			String message= "Missing data "+e.getMissingHash();
			log.debug(message);
		}
		
		// TODO: possibly fire off request to specific Peer? Unclear if this improves things generally, but might avoid polling
		
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy