/*
* Copyright 2013 Thomas Bocek
*
* 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 net.tomp2p.peers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import net.tomp2p.utils.CacheMap;
import net.tomp2p.utils.ConcurrentCacheMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This routing implementation uses is based on Kademlia. However, many changes have been applied to make it faster and
* more flexible. This class is partially thread-safe.
*
* @author Thomas Bocek
*/
public class PeerMap implements PeerStatusListener, Maintainable {
private static final Logger LOG = LoggerFactory.getLogger(PeerMap.class);
// each distance bit has its own bag this is the size of the verified peers (the ones that we know are reachable)
private final int bagSizeVerified;
// the id of this node
private final Number160 self;
// the storage for the peers that are verified
private final List<Map<Number160, PeerStatatistic>> peerMapVerified;
// the storage for the peers that are not verified or overflown
private final List<Map<Number160, PeerStatatistic>> peerMapOverflow;
private final ConcurrentCacheMap<Number160, PeerAddress> offlineMap;
// stores listeners that will be notified if a peer gets removed or added
private final List<PeerMapChangeListener> peerMapChangeListeners = new ArrayList<PeerMapChangeListener>();
private final PeerFilter peerFilter;
// the number of failures until a peer is considered offline
private final int offlineCount;
private final Maintenance maintenance;
/**
* Creates the bag for the peers. This peer knows a lot about close peers and the further away the peers are, the
* less known they are. Distance is measured with XOR of the peer ID. The distance of peer with ID 0x12 and peer
* with Id 0x28 is 0x3a.
*
* @param peerMapConfiguration
* The configuration values of this map
*
*/
public PeerMap(final PeerMapConfiguration peerMapConfiguration) {
this.self = peerMapConfiguration.self();
if (self == null || self.isZero()) {
throw new IllegalArgumentException("Zero or null are not a valid IDs");
}
this.bagSizeVerified = peerMapConfiguration.bagSizeVerified();
this.offlineCount = peerMapConfiguration.offlineCount();
this.peerFilter = peerMapConfiguration.peerFilter();
this.peerMapVerified = initFixedMap(bagSizeVerified, false);
this.peerMapOverflow = initFixedMap(peerMapConfiguration.bagSizeOverflow(), true);
// bagSizeVerified * Number160.BITS should be enough
this.offlineMap = new ConcurrentCacheMap<Number160, PeerAddress>(
peerMapConfiguration.offlineTimeout(), bagSizeVerified * Number160.BITS);
this.maintenance = peerMapConfiguration.maintenance().init(peerMapVerified, peerMapOverflow,
offlineMap);
}
/**
* Create a fixed size bag with an unmodifiable map.
*
* @param bagSize
* The bag size
* @param caching
* If a caching map should be created
* @return The list of bags containing an unmodifiable map
*/
private List<Map<Number160, PeerStatatistic>> initFixedMap(final int bagSize, final boolean caching) {
List<Map<Number160, PeerStatatistic>> tmp = new ArrayList<Map<Number160, PeerStatatistic>>();
for (int i = 0; i < Number160.BITS; i++) {
// I made some experiments here and concurrent sets are not
// necessary, as we divide similar to segments aNonBlockingHashSets
// in a concurrent map. In a full network, we have 160 segments, for
// smaller we see around 3-4 segments, growing with the number of
// peers. bags closer to 0 will see more read than write, and bags
// closer to 160 will see more writes than reads.
//
// We also only allocate memory for the bags far away, as they are likely to be filled first.
if (caching) {
tmp.add(new CacheMap<Number160, PeerStatatistic>(bagSize, true));
} else {
final int memAlloc = Math.max(0, bagSize - (Number160.BITS - i));
tmp.add(new HashMap<Number160, PeerStatatistic>(memAlloc));
}
}
return Collections.unmodifiableList(tmp);
}
/**
* Add a map change listener. This is thread-safe
*
* @param peerMapChangeListener
* The listener
*/
public void addPeerMapChangeListener(final PeerMapChangeListener peerMapChangeListener) {
synchronized (peerMapChangeListeners) {
peerMapChangeListeners.add(peerMapChangeListener);
}
}
/**
* Remove a map change listener. This is thread-safe
*
* @param peerMapChangeListener
* The listener
*/
public void removePeerMapChangeListener(final PeerMapChangeListener peerMapChangeListener) {
synchronized (peerMapChangeListeners) {
peerMapChangeListeners.add(peerMapChangeListener);
}
}
/**
* Notifies on insert. Since listeners are never changed, this is thread safe.
*
* @param peerAddress
* The address of the inserted peers
* @param verified
* True if the peer was inserted in the verified map
*/
private void notifyInsert(final PeerAddress peerAddress, final boolean verified) {
synchronized (peerMapChangeListeners) {
for (PeerMapChangeListener listener : peerMapChangeListeners) {
listener.peerInserted(peerAddress, verified);
}
}
}
/**
* Notifies on remove. Since listeners are never changed, this is thread safe.
*
* @param peerAddress
* The address of the removed peers
* @param storedPeerAddress
* Contains information statistical information
*/
private void notifyRemove(final PeerAddress peerAddress, final PeerStatatistic storedPeerAddress) {
synchronized (peerMapChangeListeners) {
for (PeerMapChangeListener listener : peerMapChangeListeners) {
listener.peerRemoved(peerAddress, storedPeerAddress);
}
}
}
/**
* Notifies on update. This method is thread safe.
*
* @param peerAddress
* The address of the updated peers.
* @param storedPeerAddress
* Contains information statistical information
*/
private void notifyUpdate(final PeerAddress peerAddress, final PeerStatatistic storedPeerAddress) {
synchronized (peerMapChangeListeners) {
for (PeerMapChangeListener listener : peerMapChangeListeners) {
listener.peerUpdated(peerAddress, storedPeerAddress);
}
}
}
/**
* The number of the peers in the verified map.
*
* @return the total number of peers
*/
public int size() {
int size = 0;
for (Map<Number160, PeerStatatistic> map : peerMapVerified) {
synchronized (map) {
size += map.size();
}
}
return size;
}
/**
* Each node that has a bag has an ID itself to define what is close. This method returns this ID.
*
* @return The id of this node
*/
public Number160 self() {
return self;
}
/**
* Adds a neighbor to the neighbor list. If the bag is full, the id zero or the same as our id, the neighbor is not
* added. This method is tread-safe
*
* @param remotePeer
* The node that should be added
* @param referrer
* If we had direct contact and we know for sure that this node is online, we set firsthand to true.
* Information from 3rd party peers are always second hand and treated as such
* @return True if the neighbor could be added or updated, otherwise false.
*/
@Override
public boolean peerFound(final PeerAddress remotePeer, final PeerAddress referrer) {
boolean firstHand = referrer == null;
// always trust first hand information
if (firstHand) {
offlineMap.remove(remotePeer.getPeerId());
}
// don't add nodes with zero node id, do not add myself and do not add
// nodes marked as bad
if (remotePeer.getPeerId().isZero() || self().equals(remotePeer.getPeerId())
|| offlineMap.containsKey(remotePeer.getPeerId()) || peerFilter.reject(remotePeer)) {
return false;
}
final int classMember = classMember(remotePeer.getPeerId());
// the peer might have a new port
final PeerStatatistic oldPeerStatatistic = updateExistingVerifiedPeerAddress(
peerMapVerified.get(classMember), remotePeer, firstHand);
if (oldPeerStatatistic != null) {
// we update the peer, so we can exit here and report that we have
// updated it.
notifyUpdate(remotePeer, oldPeerStatatistic);
return true;
} else {
if (firstHand) {
final Map<Number160, PeerStatatistic> map = peerMapVerified.get(classMember);
boolean insterted = false;
synchronized (map) {
// check again, now we are synchronized
if (map.containsKey(remotePeer.getPeerId())) {
return peerFound(remotePeer, referrer);
}
if (map.size() < bagSizeVerified) {
final PeerStatatistic peerStatatistic = new PeerStatatistic(remotePeer);
peerStatatistic.successfullyChecked();
map.put(remotePeer.getPeerId(), peerStatatistic);
insterted = true;
}
}
if (insterted) {
// if we inserted into the verified map, remove it from the non-verified map
final Map<Number160, PeerStatatistic> mapOverflow = peerMapOverflow.get(classMember);
synchronized (mapOverflow) {
mapOverflow.remove(remotePeer.getPeerId());
}
notifyInsert(remotePeer, true);
return true;
}
}
}
// if we are here, we did not have this peer, but our verified map was full
// check if we have it stored in the non verified map.
final Map<Number160, PeerStatatistic> mapOverflow = peerMapOverflow.get(classMember);
synchronized (mapOverflow) {
PeerStatatistic peerStatatistic = mapOverflow.get(remotePeer.getPeerId());
if (peerStatatistic == null) {
peerStatatistic = new PeerStatatistic(remotePeer);
}
if (firstHand) {
peerStatatistic.successfullyChecked();
}
mapOverflow.put(remotePeer.getPeerId(), peerStatatistic);
}
notifyInsert(remotePeer, false);
return true;
}
/**
* Remove a peer from the list. In order to not reappear, the node is put for a certain time in a cache list to keep
* the node removed. This method is thread-safe.
*
* @param remotePeer
* The node that should be removed
* @param force
* A flag that removes a peer immediately.
* @return True if the neighbor was removed and added to a cache list. False if peer has not been removed or is
* already in the peer removed temporarily list.
*/
@Override
public boolean peerFailed(final PeerAddress remotePeer, final boolean force) {
if (LOG.isDebugEnabled()) {
LOG.debug("peer " + remotePeer + " is offline");
}
// TB: ignore ZERO peer Id for the moment, but we should filter for the IP address
if (remotePeer.getPeerId().isZero() || self().equals(remotePeer.getPeerId())) {
return false;
}
final int classMember = classMember(remotePeer.getPeerId());
if (force) {
offlineMap.put(remotePeer.getPeerId(), remotePeer);
Map<Number160, PeerStatatistic> tmp = peerMapOverflow.get(classMember);
if (tmp != null) {
synchronized (tmp) {
tmp.remove(remotePeer.getPeerId());
}
}
tmp = peerMapVerified.get(classMember);
if (tmp != null) {
boolean removed = false;
final PeerStatatistic peerStatatistic;
synchronized (tmp) {
peerStatatistic = tmp.remove(remotePeer.getPeerId());
if (peerStatatistic != null) {
removed = true;
}
}
if (removed) {
notifyRemove(remotePeer, peerStatatistic);
return true;
}
}
return false;
}
// not forced
if (updatePeerStatistic(remotePeer, peerMapVerified.get(classMember), offlineCount)) {
return peerFailed(remotePeer, true);
}
if (updatePeerStatistic(remotePeer, peerMapOverflow.get(classMember), offlineCount)) {
return peerFailed(remotePeer, true);
}
return false;
}
/**
* Checks if a peer address in either in the verified map.
*
* @param peerAddress
* The peer address to check
* @return True, if the peer address is either in the verified map
*/
public boolean contains(final PeerAddress peerAddress) {
final int classMember = classMember(peerAddress.getPeerId());
if (classMember == -1) {
// -1 means we searched for ourself and we never are our neighbor
return false;
}
final Map<Number160, PeerStatatistic> tmp = peerMapVerified.get(classMember);
synchronized (tmp) {
return tmp.containsKey(peerAddress.getPeerId());
}
}
/**
* Checks if a peer address in either in the overflow / non-verified map.
*
* @param peerAddress
* The peer address to check
* @return True, if the peer address is either in the overflow / non-verified map
*/
public boolean containsOverflow(final PeerAddress peerAddress) {
final int classMember = classMember(peerAddress.getPeerId());
if (classMember == -1) {
// -1 means we searched for ourself and we never are our neighbor
return false;
}
final Map<Number160, PeerStatatistic> tmp = peerMapOverflow.get(classMember);
synchronized (tmp) {
return tmp.containsKey(peerAddress.getPeerId());
}
}
/**
* Returns close peer from the set to a given key. This method is tread-safe. You can use the returned set as its a
* copy of the actual PeerMap and changes in the return set do not affect PeerMap.
*
* @param id
* The key that should be close to the keys in the map
* @param atLeast
* The number we want to find at least
* @return A navigable set with close peers first in this set.
*/
public SortedSet<PeerAddress> closePeers(final Number160 id, final int atLeast) {
final SortedSet<PeerAddress> set = new TreeSet<PeerAddress>(createComparator(id));
final int classMember = classMember(id);
// special treatment, as we can start iterating from 0
if (classMember == -1) {
for (int j = 0; j < Number160.BITS; j++) {
final Map<Number160, PeerStatatistic> tmp = peerMapVerified.get(j);
if (fillSet(atLeast, set, tmp)) {
return set;
}
}
return set;
}
Map<Number160, PeerStatatistic> tmp = peerMapVerified.get(classMember);
if (fillSet(atLeast, set, tmp)) {
return set;
}
// in this case we have to go over all the bags that are smaller
boolean last = false;
for (int i = 0; i < classMember; i++) {
tmp = peerMapVerified.get(i);
last = fillSet(atLeast, set, tmp);
}
if (last) {
return set;
}
// in this case we have to go over all the bags that are larger
for (int i = classMember + 1; i < Number160.BITS; i++) {
tmp = peerMapVerified.get(i);
fillSet(atLeast, set, tmp);
}
return set;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("I'm node ");
sb.append(self()).append("\n");
for (int i = 0; i < Number160.BITS; i++) {
final Map<Number160, PeerStatatistic> tmp = peerMapVerified.get(i);
synchronized (tmp) {
if (tmp.size() > 0) {
sb.append("class:").append(i).append("->\n");
for (final PeerStatatistic node : tmp.values()) {
sb.append("node:").append(node.getPeerAddress()).append(",");
}
}
}
}
return sb.toString();
}
/**
* Creates an XOR comparator based on this peer ID.
*
* @return The XOR comparator
*/
public Comparator<PeerAddress> createComparator() {
return createComparator(self);
}
/**
* Return all addresses from the neighbor list. The collection is a copy and it is partially sorted.
*
* @return All neighbors
*/
public List<PeerAddress> getAll() {
List<PeerAddress> all = new ArrayList<PeerAddress>();
for (Map<Number160, PeerStatatistic> map : peerMapVerified) {
synchronized (map) {
for (PeerStatatistic peerStatatistic : map.values()) {
all.add(peerStatatistic.getPeerAddress());
}
}
}
return all;
}
/**
* Return all addresses from the overflow / non-verified list. The collection is a copy and it is partially sorted.
*
* @return All neighbors
*/
public List<PeerAddress> getAllOverflow() {
List<PeerAddress> all = new ArrayList<PeerAddress>();
for (Map<Number160, PeerStatatistic> map : peerMapOverflow) {
synchronized (map) {
for (PeerStatatistic peerStatatistic : map.values()) {
all.add(peerStatatistic.getPeerAddress());
}
}
}
return all;
}
/**
* Checks if a peer is in the offline map.
*
* @param peerAddress
* The address to look for
* @return True if the peer is in the offline map, meaning that we consider this peer offline.
*/
public boolean isPeerRemovedTemporarly(final PeerAddress peerAddress) {
return offlineMap.containsKey(peerAddress.getPeerId());
}
/**
* Finds the next peer that should have a maintenance check. Returns null if no maintenance is needed at the moment.
* It will return the most important peers first. Importance is as follows: The most important peers are the close
* ones in the verified peer map. If a certain threshold in a bag is not reached, the unverified becomes important
* too.
*
* @return The next most important peer to check if its still alive.
*/
public PeerStatatistic nextForMaintenance(Collection<PeerAddress> notInterestedAddresses) {
return maintenance.nextForMaintenance(notInterestedAddresses);
}
/**
* Returns the number of the class that this id belongs to.
*
* @param remoteID
* The id to test
* @return The number of bits used in the difference.
*/
private int classMember(final Number160 remoteID) {
return classMember(self(), remoteID);
}
/**
* Returns -1 if the first remote node is closer to the key, if the second is closer, then 1 is returned. If both
* are equal, 0 is returned
*
* @param id
* The key to search for
* @param rn
* The remote node on the routing path to node close to key
* @param rn2
* An other remote node on the routing path to node close to key
* @return -1 if nodeAddress1 is closer to the key than nodeAddress2, otherwise 1. 0 is returned if both are equal.
*/
public static int isCloser(final Number160 id, final PeerAddress rn, final PeerAddress rn2) {
return isKadCloser(id, rn, rn2);
}
/**
* Returns -1 if the first key is closer to the key, if the second is closer, then 1 is returned. If both are equal,
* 0 is returned
*
* @param id
* The key to search for
* @param rn
* The first key
* @param rn2
* The second key
* @return -1 if key1 is closer to key, otherwise 1. 0 is returned if both are equal.
*/
public static int isCloser(final Number160 id, final Number160 rn, final Number160 rn2) {
return distance(id, rn).compareTo(distance(id, rn2));
}
/**
* @see PeerMap.routing.Routing#isCloser(java.math.BigInteger, PeerAddress.routing.NodeAddress,
* PeerAddress.routing.NodeAddress)
* @param key
* The key to search for
* @param rn2
* The remote node on the routing path to node close to key
* @param rn
* An other remote node on the routing path to node close to key
* @return True if rn2 is closer or has the same distance to key as rn
*/
/**
* Returns -1 if the first remote node is closer to the key, if the secondBITS is closer, then 1 is returned. If
* both are equal, 0 is returned
*
* @param id
* The id as a distance reference
* @param rn
* The peer to test if closer to the id
* @param rn2
* The other peer to test if closer to the id
* @return -1 if first peer is closer, 1 otherwise, 0 if both are equal
*/
public static int isKadCloser(final Number160 id, final PeerAddress rn, final PeerAddress rn2) {
return distance(id, rn.getPeerId()).compareTo(distance(id, rn2.getPeerId()));
}
/**
* Create the Kademlia distance comparator.
*
* @param id
* The id of this peer
* @return The XOR comparator
*/
public static Comparator<PeerAddress> createComparator(final Number160 id) {
return new Comparator<PeerAddress>() {
public int compare(final PeerAddress remotePeer, final PeerAddress remotePeer2) {
return isKadCloser(id, remotePeer, remotePeer2);
}
};
}
/**
* Returns the difference in terms of bit counts of two ids, minus 1. So two IDs with one bit difference are in the
* class 0.
*
* @param id1
* The first id
* @param id2
* The second id
* @return returns the bit difference and -1 if they are equal
*/
static int classMember(final Number160 id1, final Number160 id2) {
return distance(id1, id2).bitLength() - 1;
}
/**
* The distance metric is the XOR metric.
*
* @param id1
* The first id
* @param id2
* The second id
* @return The distance
*/
static Number160 distance(final Number160 id1, final Number160 id2) {
return id1.xor(id2);
}
/**
* Updates the peer statistics and checks if the max failure has been reached.
*
* @param remotePeer
* The remote peer
* @param tmp
* The bag of where the peer is supposed to be
* @param maxFail
* The number of max failure until a peer is considered offline
* @return True if this peer is considered offline, otherwise false
*/
private static boolean updatePeerStatistic(final PeerAddress remotePeer,
final Map<Number160, PeerStatatistic> tmp, final int maxFail) {
if (tmp != null) {
synchronized (tmp) {
PeerStatatistic peerStatatistic = tmp.get(remotePeer.getPeerId());
if (peerStatatistic != null) {
if (peerStatatistic.failed() >= maxFail) {
return true;
}
}
}
}
return false;
}
/**
* Checks if a peer already exists in this map and if it does, it will update the entry because the peer address
* (e.g. port) may have changed.
*
* @param tmp
* The map where the peer is suppost to be
* @param peerAddress
* The address of the peer that may have been changed
* @param firstHand
* True if this peer send and received a message from the remote peer
* @return The old peer address if we have updated the peer, null otherwise
*/
private static PeerStatatistic updateExistingVerifiedPeerAddress(
final Map<Number160, PeerStatatistic> tmp, final PeerAddress peerAddress, final boolean firstHand) {
synchronized (tmp) {
PeerStatatistic old = tmp.get(peerAddress.getPeerId());
if (old != null) {
old.setPeerAddress(peerAddress);
if (firstHand) {
old.successfullyChecked();
}
return old;
}
}
return null;
}
/**
* Fills the set with peer addresses. Fills it until a limit is reach. However, this is a soft limit, as the bag may
* contain close peers in a random manner.
*
* @param atLeast
* The number of addresses we want at least. It does not matter if its more.
* @param set
* The set where to store the results
* @param tmp
* The bag where to take the addresses from
* @return True if the desired size has been reached
*/
private static boolean fillSet(final int atLeast, final SortedSet<PeerAddress> set,
final Map<Number160, PeerStatatistic> tmp) {
synchronized (tmp) {
for (final PeerStatatistic peerStatatistic : tmp.values()) {
set.add(peerStatatistic.getPeerAddress());
}
}
return set.size() >= atLeast;
}
}