diff --git a/core/src/main/java/org/bitcoinj/core/TransactionConfidence.java b/core/src/main/java/org/bitcoinj/core/TransactionConfidence.java index eae827a9..78978fa2 100644 --- a/core/src/main/java/org/bitcoinj/core/TransactionConfidence.java +++ b/core/src/main/java/org/bitcoinj/core/TransactionConfidence.java @@ -174,6 +174,15 @@ public class TransactionConfidence implements Serializable { public void onConfidenceChanged(TransactionConfidence confidence, ChangeReason reason); } + // This is used to ensure that confidence objects which aren't referenced from anywhere but which have an event + // listener set on them don't become eligible for garbage collection. Otherwise the TxConfidenceTable, which only + // has weak references to these objects, would not be enough to keep the event listeners working as transactions + // propagate around the network - it cannot know directly if the API user is interested in the object, so it uses + // heap reachability as a proxy for interest. + // + // We add ourselves to this set when a listener is added and remove ourselves when the listener list is empty. + private static final Set pinnedConfidenceObjects = Collections.synchronizedSet(new HashSet()); + /** *

Adds an event listener that will be run when this confidence object is updated. The listener will be locked and * is likely to be invoked on a peer thread.

@@ -186,6 +195,7 @@ public class TransactionConfidence implements Serializable { public void addEventListener(Listener listener, Executor executor) { checkNotNull(listener); listeners.addIfAbsent(new ListenerRegistration(listener, executor)); + pinnedConfidenceObjects.add(this); } /** @@ -204,7 +214,10 @@ public class TransactionConfidence implements Serializable { public boolean removeEventListener(Listener listener) { checkNotNull(listener); - return ListenerRegistration.removeFromList(listener, listeners); + boolean removed = ListenerRegistration.removeFromList(listener, listeners); + if (listeners.isEmpty()) + pinnedConfidenceObjects.remove(this); + return removed; } /** diff --git a/core/src/main/java/org/bitcoinj/utils/ListenerRegistration.java b/core/src/main/java/org/bitcoinj/utils/ListenerRegistration.java index d8e61729..818d7353 100644 --- a/core/src/main/java/org/bitcoinj/utils/ListenerRegistration.java +++ b/core/src/main/java/org/bitcoinj/utils/ListenerRegistration.java @@ -33,6 +33,7 @@ public class ListenerRegistration { this.executor = checkNotNull(executor); } + /** Returns true if the listener was removed, else false. */ public static boolean removeFromList(T listener, List> list) { checkNotNull(listener); diff --git a/core/src/test/java/org/bitcoinj/core/TxConfidenceTableTest.java b/core/src/test/java/org/bitcoinj/core/TxConfidenceTableTest.java index d8a3ca79..0edce577 100644 --- a/core/src/test/java/org/bitcoinj/core/TxConfidenceTableTest.java +++ b/core/src/test/java/org/bitcoinj/core/TxConfidenceTableTest.java @@ -35,7 +35,8 @@ public class TxConfidenceTableTest { @Before public void setup() throws Exception { BriefLogFormatter.init(); - table = Context.getOrCreate().getConfidenceTable(); + Context context = new Context(); + table = context.getConfidenceTable(); Address to = new ECKey().toAddress(params); Address change = new ECKey().toAddress(params); @@ -48,7 +49,26 @@ public class TxConfidenceTableTest { address2 = new PeerAddress(InetAddress.getByAddress(new byte[] { 127, 0, 0, 2 })); address3 = new PeerAddress(InetAddress.getByAddress(new byte[] { 127, 0, 0, 3 })); } - + + @Test + public void pinHandlers() throws Exception { + Transaction tx = new Transaction(params, tx1.bitcoinSerialize()); + Sha256Hash hash = tx.getHash(); + table.seen(hash, address1); + assertEquals(1, tx.getConfidence().numBroadcastPeers()); + final int[] seen = new int[1]; + tx.getConfidence().addEventListener(new TransactionConfidence.Listener() { + @Override + public void onConfidenceChanged(TransactionConfidence confidence, ChangeReason reason) { + seen[0] = confidence.numBroadcastPeers(); + } + }, Threading.SAME_THREAD); + tx = null; + System.gc(); + table.seen(hash, address2); + assertEquals(2, seen[0]); + } + @Test public void invAndDownload() throws Exception { // Base case: we see a transaction announced twice and then download it. The count is in the confidence object.