3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-01-30 23:02:15 +00:00

Introduce a PeerGroup method for calculating the most common chain height.

Use it to make peer selection a little smarter.
Render pending peers and peers that are reporting un-common chain heights in PeerMonitor.
This commit is contained in:
Mike Hearn 2012-12-24 22:32:16 +00:00
parent 2a55c58460
commit 07011be796
3 changed files with 191 additions and 22 deletions

View File

@ -69,9 +69,10 @@ public class PeerGroup extends AbstractIdleService {
// These lists are all thread-safe so do not have to be accessed under the PeerGroup lock.
// Addresses to try to connect to, excluding active peers.
private List<PeerAddress> inactives;
// Currently active peers. This is an ordered list rather than a set to make unit tests predictable.
// Currently active peers. This is an ordered list rather than a set to make unit tests predictable. This is a
// synchronized list. Locking order is: PeerGroup < Peer < peers. Same for pendingPeers.
private List<Peer> peers;
// Currently connecting peers
// Currently connecting peers.
private List<Peer> pendingPeers;
private Map<Peer, ChannelFuture> channelFutures;
@ -386,9 +387,16 @@ public class PeerGroup extends AbstractIdleService {
*/
public List<Peer> getConnectedPeers() {
synchronized (peers) {
ArrayList<Peer> result = new ArrayList<Peer>(peers.size());
result.addAll(peers);
return result;
return new ArrayList<Peer>(peers);
}
}
/**
* Returns a list containing Peers that did not complete connection yet.
*/
public List<Peer> getPendingPeers() {
synchronized (pendingPeers) {
return new ArrayList<Peer>(pendingPeers);
}
}
@ -638,12 +646,11 @@ public class PeerGroup extends AbstractIdleService {
// Link the peer to the memory pool so broadcast transactions have their confidence levels updated.
peer.setMemoryPool(memoryPool);
// If we want to download the chain, and we aren't currently doing so, do so now.
// TODO: Change this so we automatically switch the download peer based on ping times.
if (downloadListener != null && downloadPeer == null) {
if (downloadListener != null && downloadPeer == null && chain != null) {
log.info(" starting block chain download");
startBlockChainDownloadFromPeer(peer);
} else if (downloadPeer == null) {
setDownloadPeer(peer);
setDownloadPeer(selectDownloadPeer(peers));
} else {
peer.setDownloadData(false);
}
@ -741,8 +748,12 @@ public class PeerGroup extends AbstractIdleService {
}
private synchronized void setDownloadPeer(Peer peer) {
if (chain == null)
if (chain == null) {
// PeerGroup creator did not want us to download any data. We still track the download peer for
// informational purposes.
downloadPeer = peer;
return;
}
if (downloadPeer != null) {
log.info("Unsetting download peer: {}", downloadPeer);
downloadPeer.setDownloadData(false);
@ -805,12 +816,13 @@ public class PeerGroup extends AbstractIdleService {
log.info("Download peer died. Picking a new one.");
setDownloadPeer(null);
// Pick a new one and possibly tell it to download the chain.
// TODO: Fix lock inversion here.
synchronized (peers) {
if (!peers.isEmpty()) {
Peer next = peers.get(0);
setDownloadPeer(next);
Peer newDownloadPeer = selectDownloadPeer(peers);
if (newDownloadPeer != null) {
setDownloadPeer(newDownloadPeer);
if (downloadListener != null) {
startBlockChainDownloadFromPeer(next);
startBlockChainDownloadFromPeer(newDownloadPeer);
}
}
}
@ -932,6 +944,7 @@ public class PeerGroup extends AbstractIdleService {
public void run() {
// This can be called immediately if we already have enough peers. Otherwise it'll be called from a
// peer thread.
// TODO: Fix the race that exists here.
final Peer somePeer = peers.get(0);
log.info("broadcastTransaction: Enough peers, adding {} to the memory pool and sending to {}",
tx.getHashAsString(), somePeer);
@ -1034,6 +1047,67 @@ public class PeerGroup extends AbstractIdleService {
this.pingIntervalMsec = pingIntervalMsec;
}
/**
* Returns our peers most commonly reported chain height. If multiple heights are tied, the highest is returned.
* If no peers are connected, returns zero.
*/
public int getMostCommonChainHeight() {
// Copy the peers list so we can calculate on it without violating lock ordering: Peer < peers
ArrayList<Peer> peers;
synchronized (this.peers) {
peers = new ArrayList<Peer>(this.peers);
}
if (peers.isEmpty())
return 0;
int s = peers.size();
int[] heights = new int[s];
int[] counts = new int[s];
int maxCount = 0;
// Calculate the frequencies of each reported height.
for (Peer peer : peers) {
int h = (int) peer.getBestHeight();
// Find the index of the peers height in the heights array.
for (int cursor = 0; cursor < s; cursor++) {
if (heights[cursor] == h) {
maxCount = Math.max(++counts[cursor], maxCount);
break;
} else if (heights[cursor] == 0) {
// A new height we didn't see before.
Preconditions.checkState(counts[cursor] == 0);
heights[cursor] = h;
counts[cursor] = 1;
maxCount = Math.max(maxCount, 1);
break;
}
}
}
// Find the heights that have the highest frequencies.
int[] freqHeights = new int[s];
int cursor = 0;
for (int i = 0; i < s; i++) {
if (counts[i] == maxCount) {
freqHeights[cursor++] = heights[i];
}
}
// Return the highest of the most common heights.
Arrays.sort(freqHeights);
return freqHeights[s - 1];
}
/** Given a list of Peers, return a Peer to be used as the download peer. */
protected Peer selectDownloadPeer(List<Peer> peers) {
synchronized (peers) {
if (peers.isEmpty())
return null;
// Make sure we don't select a peer that is behind/synchronizing itself.
int mostCommonChainHeight = getMostCommonChainHeight();
for (Peer peer : peers) {
if (peer.getBestHeight() == mostCommonChainHeight) return peer;
}
throw new IllegalStateException("Unreachable");
}
}
private static class PeerGroupThreadFactory implements ThreadFactory {
static final AtomicInteger poolNumber = new AtomicInteger(1);
final ThreadGroup group;

View File

@ -20,6 +20,7 @@ import com.google.bitcoin.discovery.PeerDiscovery;
import com.google.bitcoin.discovery.PeerDiscoveryException;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@ -71,6 +72,11 @@ public class PeerGroupTest extends TestWithNetworkConnections {
peerGroup.setPingIntervalMsec(0); // Disable the pings as they just get in the way of most tests.
}
@After
public void shutDown() throws Exception {
peerGroup.stopAndWait();
}
@Test
public void listener() throws Exception {
AbstractPeerEventListener listener = new AbstractPeerEventListener() {
@ -395,4 +401,19 @@ public class PeerGroupTest extends TestWithNetworkConnections {
inbound(p1, new Pong(ping.getNonce()));
assertTrue(peerGroup.getConnectedPeers().get(0).getLastPingTime() < Long.MAX_VALUE);
}
@Test
public void commonChainHeights() throws Exception {
peerGroup.startAndWait();
VersionMessage versionMessage2 = new VersionMessage(params, 2);
VersionMessage versionMessage3 = new VersionMessage(params, 3);
connectPeer(1, versionMessage2);
assertEquals(2, peerGroup.getMostCommonChainHeight());
connectPeer(2, versionMessage2);
assertEquals(2, peerGroup.getMostCommonChainHeight());
connectPeer(3, versionMessage3);
assertEquals(2, peerGroup.getMostCommonChainHeight());
connectPeer(4, versionMessage3);
assertEquals(3, peerGroup.getMostCommonChainHeight());
}
}

View File

@ -27,6 +27,7 @@ import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
@ -41,6 +42,7 @@ public class PeerMonitor {
private NetworkParameters params;
private PeerGroup peerGroup;
private PeerTableModel peerTableModel;
private PeerTableRenderer peerTableRenderer;
public static void main(String[] args) throws Exception {
BriefLogFormatter.init();
@ -76,7 +78,7 @@ public class PeerMonitor {
// Tell the Swing UI thread to redraw the peers table.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
peerTableModel.fireTableDataChanged();
peerTableModel.updateFromPeerGroup();
}
});
}
@ -110,6 +112,11 @@ public class PeerMonitor {
peerTableModel = new PeerTableModel();
JTable peerTable = new JTable(peerTableModel);
peerTable.setAutoCreateRowSorter(true);
peerTableRenderer = new PeerTableRenderer(peerTableModel);
peerTable.setDefaultRenderer(String.class, peerTableRenderer);
peerTable.setDefaultRenderer(Integer.class, peerTableRenderer);
peerTable.setDefaultRenderer(Long.class, peerTableRenderer);
JScrollPane scrollPane = new JScrollPane(peerTable);
window.getContentPane().add(scrollPane, BorderLayout.CENTER);
window.pack();
@ -119,21 +126,30 @@ public class PeerMonitor {
// Refresh the UI every half second to get the latest ping times. The event handler runs in the UI thread.
new Timer(1000, new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
peerTableModel.fireTableDataChanged();
peerTableModel.updateFromPeerGroup();
}
}).start();
}
private class PeerTableModel extends AbstractTableModel {
private final int IP_ADDRESS = 0;
private final int PROTOCOL_VERSION = 1;
private final int USER_AGENT = 2;
private final int CHAIN_HEIGHT = 3;
private final int PING_TIME = 4;
private final int LAST_PING_TIME = 5;
public static final int IP_ADDRESS = 0;
public static final int PROTOCOL_VERSION = 1;
public static final int USER_AGENT = 2;
public static final int CHAIN_HEIGHT = 3;
public static final int PING_TIME = 4;
public static final int LAST_PING_TIME = 5;
public List<Peer> connectedPeers;
public List<Peer> pendingPeers;
public void updateFromPeerGroup() {
connectedPeers = peerGroup.getConnectedPeers();
pendingPeers = peerGroup.getPendingPeers();
fireTableDataChanged();
}
public int getRowCount() {
return peerGroup.numConnectedPeers();
return peerGroup.numConnectedPeers() + peerGroup.getPendingPeers().size();
}
@Override
@ -167,7 +183,25 @@ public class PeerMonitor {
}
public Object getValueAt(int row, int col) {
// This is racy. A peer can be moving from pending to connected between these two lines.
List<Peer> peers = peerGroup.getConnectedPeers();
List<Peer> pendingPeers = peerGroup.getPendingPeers();
if (row >= peers.size()) {
// Peer that isn't connected yet.
Peer peer = pendingPeers.get(row - peers.size());
switch (col) {
case IP_ADDRESS:
return peer.getAddress().getAddr().getHostAddress();
case PROTOCOL_VERSION:
return 0;
case CHAIN_HEIGHT:
case PING_TIME:
case LAST_PING_TIME:
return 0L;
default:
return "(pending)";
}
}
Peer peer = peers.get(row);
switch (col) {
case IP_ADDRESS:
@ -190,4 +224,44 @@ public class PeerMonitor {
}
}
}
private class PeerTableRenderer extends JLabel implements TableCellRenderer {
private final PeerTableModel model;
private final Font normal, bold;
public PeerTableRenderer(PeerTableModel model) {
super();
this.model = model;
this.normal = new Font("Sans Serif", Font.PLAIN, 12);
this.bold = new Font("Sans Serif", Font.BOLD, 12);
}
public Component getTableCellRendererComponent(JTable table, Object contents,
boolean selected, boolean hasFocus, int row, int column) {
setText(contents.toString());
if (model.connectedPeers == null || model.pendingPeers == null) {
return this;
}
if (row >= model.connectedPeers.size()) {
setFont(normal);
setForeground(Color.LIGHT_GRAY);
} else {
if (model.connectedPeers.get(row).getDownloadData())
setFont(bold);
else
setFont(normal);
setForeground(Color.BLACK);
// Mark chain heights that aren't normal.
if (column == PeerTableModel.CHAIN_HEIGHT) {
long height = (Long) contents;
if (height != peerGroup.getMostCommonChainHeight()) {
setText(height + "");
}
}
}
return this;
}
}
}