Run wallet event listeners unlocked. Resolves another inversion.

Update issue 223.
This commit is contained in:
Mike Hearn 2013-03-07 17:07:44 +01:00
parent 0c30050a97
commit 0534231de9
6 changed files with 86 additions and 76 deletions

View File

@ -19,7 +19,6 @@ package com.google.bitcoin.core;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.core.WalletTransaction.Pool;
import com.google.bitcoin.store.WalletProtobufSerializer;
import com.google.bitcoin.utils.EventListenerInvoker;
import com.google.bitcoin.utils.Locks;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
@ -293,14 +292,15 @@ public class Wallet implements Serializable, BlockChainListener {
inactive = new HashMap<Sha256Hash, Transaction>();
pending = new HashMap<Sha256Hash, Transaction>();
dead = new HashMap<Sha256Hash, Transaction>();
eventListeners = new CopyOnWriteArrayList<WalletEventListener>();
createTransientState();
}
private void createTransientState() {
eventListeners = new CopyOnWriteArrayList<WalletEventListener>();
ignoreNextNewBlock = new HashSet<Sha256Hash>();
txConfidenceListener = new TransactionConfidence.Listener() {
public void onConfidenceChanged(Transaction tx) {
lock.lock();
invokeOnTransactionConfidenceChanged(tx);
// Many onWalletChanged events will not occur because they are suppressed, eg, because:
// - we are inside a re-org
@ -312,6 +312,7 @@ public class Wallet implements Serializable, BlockChainListener {
// The latter case cannot happen today because we won't hear about it, but in future this may
// become more common if conflict notices are implemented.
invokeOnWalletChanged();
lock.unlock();
}
};
acceptTimeLockedTransactions = false;
@ -938,23 +939,6 @@ public class Wallet implements Serializable, BlockChainListener {
}
}
// Boilerplate that allows event listeners to delete themselves during execution, and auto locks the listener.
private void invokeOnCoinsReceived(final Transaction tx, final BigInteger balance, final BigInteger newBalance) {
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker<WalletEventListener>() {
@Override public void invoke(WalletEventListener listener) {
listener.onCoinsReceived(Wallet.this, tx, balance, newBalance);
}
});
}
private void invokeOnCoinsSent(final Transaction tx, final BigInteger prevBalance, final BigInteger newBalance) {
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker<WalletEventListener>() {
@Override public void invoke(WalletEventListener listener) {
listener.onCoinsSent(Wallet.this, tx, prevBalance, newBalance);
}
});
}
/**
* <p>Returns true if the given transaction sends coins to any of our keys, or has inputs spending any of our outputs,
* and if includeDoubleSpending is true, also returns true if tx has inputs that are spending outputs which are
@ -1134,8 +1118,6 @@ public class Wallet implements Serializable, BlockChainListener {
* <p/>
* <p>Used to update confidence data in each transaction and last seen block hash. Triggers auto saving.
* Invokes the onWalletChanged event listener if there were any affected transactions.</p>
*
* @param block
*/
public void notifyNewBestBlock(StoredBlock block) throws VerificationException {
// Check to see if this block has been seen before.
@ -1957,28 +1939,28 @@ public class Wallet implements Serializable, BlockChainListener {
* in the list that was not already present.
*/
public int addKeys(final List<ECKey> keys) {
int added = 0;
lock.lock();
try {
// TODO: Consider making keys a sorted list or hashset so membership testing is faster.
int added = 0;
for (final ECKey key : keys) {
if (keychain.contains(key)) continue;
keychain.add(key);
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker<WalletEventListener>() {
@Override
public void invoke(WalletEventListener listener) {
listener.onKeyAdded(key);
}
});
added++;
}
if (autosaveToFile != null) {
autoSave();
}
return added;
} finally {
lock.unlock();
}
for (ECKey key : keys) {
// TODO: Change this interface to be batch-oriented.
for (WalletEventListener listener : eventListeners) {
listener.onKeyAdded(key);
}
}
return added;
}
/**
@ -2429,14 +2411,8 @@ public class Wallet implements Serializable, BlockChainListener {
}
log.info("post-reorg balance is {}", Utils.bitcoinValueToFriendlyString(getBalance()));
// Inform event listeners that a re-org took place. They should save the wallet at this point.
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker<WalletEventListener>() {
@Override
public void invoke(WalletEventListener listener) {
listener.onReorganize(Wallet.this);
}
});
invokeOnReorganize();
onWalletChangedSuppressions--;
invokeOnWalletChanged();
checkState(isConsistent());
@ -2519,35 +2495,6 @@ public class Wallet implements Serializable, BlockChainListener {
}
}
private void invokeOnTransactionConfidenceChanged(final Transaction tx) {
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker<WalletEventListener>() {
@Override
public void invoke(WalletEventListener listener) {
listener.onTransactionConfidenceChanged(Wallet.this, tx);
}
});
}
private int onWalletChangedSuppressions;
private void invokeOnWalletChanged() {
lock.lock();
try {
// Don't invoke the callback in some circumstances, eg, whilst we are re-organizing or fiddling with
// transactions due to a new block arriving. It will be called later instead.
Preconditions.checkState(onWalletChangedSuppressions >= 0);
if (onWalletChangedSuppressions > 0) return;
// Call with the wallet locked.
EventListenerInvoker.invoke(eventListeners, new EventListenerInvoker<WalletEventListener>() {
@Override
public void invoke(WalletEventListener listener) {
listener.onWalletChanged(Wallet.this);
}
});
} finally {
lock.unlock();
}
}
/**
* Returns an immutable view of the transactions currently waiting for network confirmations.
*/
@ -2712,4 +2659,73 @@ public class Wallet implements Serializable, BlockChainListener {
lock.unlock();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Boilerplate for running event listeners - unlocks the wallet, runs, re-locks.
private void invokeOnTransactionConfidenceChanged(Transaction tx) {
checkState(lock.isLocked());
lock.unlock();
try {
for (WalletEventListener listener : eventListeners) {
listener.onTransactionConfidenceChanged(this, tx);
}
} finally {
lock.lock();
}
}
private int onWalletChangedSuppressions = 0;
private void invokeOnWalletChanged() {
// Don't invoke the callback in some circumstances, eg, whilst we are re-organizing or fiddling with
// transactions due to a new block arriving. It will be called later instead.
checkState(lock.isLocked());
Preconditions.checkState(onWalletChangedSuppressions >= 0);
if (onWalletChangedSuppressions > 0) return;
lock.unlock();
try {
for (WalletEventListener listener : eventListeners) {
listener.onWalletChanged(this);
}
} finally {
lock.lock();
}
}
private void invokeOnCoinsReceived(Transaction tx, BigInteger balance, BigInteger newBalance) {
checkState(lock.isLocked());
lock.unlock();
try {
for (WalletEventListener listener : eventListeners) {
listener.onCoinsReceived(Wallet.this, tx, balance, newBalance);
}
} finally {
lock.lock();
}
}
private void invokeOnCoinsSent(Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
checkState(lock.isLocked());
lock.unlock();
try {
for (WalletEventListener listener : eventListeners) {
listener.onCoinsSent(Wallet.this, tx, prevBalance, newBalance);
}
} finally {
lock.lock();
}
}
private void invokeOnReorganize() {
checkState(lock.isLocked());
lock.unlock();
try {
for (WalletEventListener listener : eventListeners) {
listener.onReorganize(Wallet.this);
}
} finally {
lock.lock();
}
}
}

View File

@ -21,9 +21,6 @@ import java.math.BigInteger;
/**
* <p>Implementors are called when the contents of the wallet changes, for instance due to receiving/sending money
* or a block chain re-organize. It may be convenient to derive from {@link AbstractWalletEventListener} instead.</p>
*
* <p>It is safe to call methods of the wallet during event listener execution, and also for a listener to remove itself.
* Other types of modifications generally aren't safe.</p>
*/
public interface WalletEventListener {
/**
@ -72,7 +69,6 @@ public interface WalletEventListener {
*/
void onReorganize(Wallet wallet);
// TODO: Flesh out the docs below some more to clarify what happens during re-orgs and other edge cases.
/**
* <p>Called on a Peer thread when a transaction changes its confidence level. You can also attach event listeners to
* the individual transactions, if you don't care about all of them. Usually you would save the wallet to disk after
@ -113,9 +109,7 @@ public interface WalletEventListener {
*
* <p>When this is called you can refresh the UI contents from the wallet contents. It's more efficient to use
* this rather than onTransactionConfidenceChanged() + onReorganize() because you only get one callback per block
* rather than one per transaction per block. Note that this is <b>not</b> called when a key is added. The wallet
* <b>is locked</b> whilst this handler is invoked, but if you relay the callback into another thread (eg the
* main UI thread) you should ensure to lock the wallet in the new thread as well.</p>
* rather than one per transaction per block. Note that this is <b>not</b> called when a key is added. </p>
*/
void onWalletChanged(Wallet wallet);

View File

@ -111,7 +111,7 @@ public class PingService {
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
// Running on a peer thread.
// MUST BE THREAD SAFE
assert !newBalance.equals(BigInteger.ZERO);
if (!tx.isPending()) return;
// It was broadcast, but we can't really verify it's valid until it appears in a block.

View File

@ -44,7 +44,7 @@ public class RefreshWallet {
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
public synchronized void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
System.out.println("\nReceived tx " + tx.getHashAsString());
System.out.println(tx.toString());
}

View File

@ -205,7 +205,7 @@ public class ToyWallet {
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onWalletChanged(Wallet wallet) {
// This is running in some arbitrary bitcoinj provided thread with the wallet locked.
// MUST BE THREAD SAFE.
final List<Transaction> txns = wallet.getTransactionsByTime();
SwingUtilities.invokeLater(new Runnable() {
public void run() {

View File

@ -503,7 +503,7 @@ public class WalletTool {
}
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onChange() {
public synchronized void onChange() {
super.onChange();
saveWallet(walletFile);
BigInteger balance = wallet.getBalance(Wallet.BalanceType.ESTIMATED);