mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-01-31 23:32:16 +00:00
Wallet: refactor saving code into a separate class that uses a Java scheduled executor rather than the messy and likely buggy custom threading code. Synchronously save the wallet on way more codepaths, now we only do write coalescing for new best block notifications.
This commit is contained in:
parent
acc2877329
commit
5219a42df9
@ -24,7 +24,7 @@ import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
|||||||
import com.google.bitcoin.store.WalletProtobufSerializer;
|
import com.google.bitcoin.store.WalletProtobufSerializer;
|
||||||
import com.google.bitcoin.utils.ListenerRegistration;
|
import com.google.bitcoin.utils.ListenerRegistration;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import com.google.common.base.Objects;
|
import com.google.bitcoin.wallet.WalletFiles;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.*;
|
import com.google.common.collect.*;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
@ -76,7 +76,7 @@ import static com.google.common.base.Preconditions.*;
|
|||||||
* that simplifies this for you although you're still responsible for manually triggering a save when your app is about
|
* that simplifies this for you although you're still responsible for manually triggering a save when your app is about
|
||||||
* to quit because the auto-save feature waits a moment before actually committing to disk to avoid IO thrashing when
|
* to quit because the auto-save feature waits a moment before actually committing to disk to avoid IO thrashing when
|
||||||
* the wallet is changing very fast (eg due to a block chain sync). See
|
* the wallet is changing very fast (eg due to a block chain sync). See
|
||||||
* {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.bitcoin.core.Wallet.AutosaveEventListener)}
|
* {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.bitcoin.wallet.WalletFiles.Listener)}
|
||||||
* for more information about this.</p>
|
* for more information about this.</p>
|
||||||
*/
|
*/
|
||||||
public class Wallet implements Serializable, BlockChainListener, PeerFilterProvider {
|
public class Wallet implements Serializable, BlockChainListener, PeerFilterProvider {
|
||||||
@ -119,16 +119,6 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
|
|
||||||
private transient CopyOnWriteArrayList<ListenerRegistration<WalletEventListener>> eventListeners;
|
private transient CopyOnWriteArrayList<ListenerRegistration<WalletEventListener>> eventListeners;
|
||||||
|
|
||||||
// Auto-save code. This all should be generalized in future to not be file specific so you can easily store the
|
|
||||||
// wallet into a database using the same mechanism. However we need to inform stores of each specific change with
|
|
||||||
// some objects representing those changes, which is more complex. To avoid poor performance in 0.6 on phones that
|
|
||||||
// have a lot of transactions in their wallet, we use the simpler approach. It's needed because the wallet stores
|
|
||||||
// the number of confirmations and accumulated work done for each transaction, so each block changes each tx.
|
|
||||||
private transient File autosaveToFile;
|
|
||||||
private transient boolean dirty; // Is a write of the wallet necessary?
|
|
||||||
private transient AutosaveEventListener autosaveEventListener;
|
|
||||||
private transient long autosaveDelayMs;
|
|
||||||
|
|
||||||
// A listener that relays confidence changes from the transaction confidence object to the wallet event listener,
|
// A listener that relays confidence changes from the transaction confidence object to the wallet event listener,
|
||||||
// as a convenience to API users so they don't have to register on every transaction themselves.
|
// as a convenience to API users so they don't have to register on every transaction themselves.
|
||||||
private transient TransactionConfidence.Listener txConfidenceListener;
|
private transient TransactionConfidence.Listener txConfidenceListener;
|
||||||
@ -146,6 +136,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
private int onWalletChangedSuppressions;
|
private int onWalletChangedSuppressions;
|
||||||
private boolean insideReorg;
|
private boolean insideReorg;
|
||||||
private Map<Transaction, TransactionConfidence.Listener.ChangeReason> confidenceChanged;
|
private Map<Transaction, TransactionConfidence.Listener.ChangeReason> confidenceChanged;
|
||||||
|
private volatile WalletFiles vFileManager;
|
||||||
|
|
||||||
/** Represents the results of a {@link CoinSelector#select(java.math.BigInteger, java.util.LinkedList)} operation */
|
/** Represents the results of a {@link CoinSelector#select(java.math.BigInteger, java.util.LinkedList)} operation */
|
||||||
public static class CoinSelection {
|
public static class CoinSelection {
|
||||||
@ -363,8 +354,10 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveToFile(File temp, File destFile) throws IOException {
|
/** Saves the wallet first to the given temp file, then renames to the dest file. */
|
||||||
|
public void saveToFile(File temp, File destFile) throws IOException {
|
||||||
FileOutputStream stream = null;
|
FileOutputStream stream = null;
|
||||||
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
stream = new FileOutputStream(temp);
|
stream = new FileOutputStream(temp);
|
||||||
saveToFileStream(stream);
|
saveToFileStream(stream);
|
||||||
@ -384,15 +377,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
} else if (!temp.renameTo(destFile)) {
|
} else if (!temp.renameTo(destFile)) {
|
||||||
throw new IOException("Failed to rename " + temp + " to " + destFile);
|
throw new IOException("Failed to rename " + temp + " to " + destFile);
|
||||||
}
|
}
|
||||||
lock.lock();
|
|
||||||
try {
|
|
||||||
if (destFile.equals(autosaveToFile)) {
|
|
||||||
dirty = false;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
|
lock.unlock();
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
stream.close();
|
stream.close();
|
||||||
}
|
}
|
||||||
@ -445,174 +431,6 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-saving can be done on a background thread if the user wishes it, this is to avoid stalling threads calling
|
|
||||||
// into the wallet on serialization/disk access all the time which is important in GUI apps where you don't want
|
|
||||||
// the main thread to ever wait on disk (otherwise you lose a lot of responsiveness). The primary case where it
|
|
||||||
// can be a problem is during block chain syncup - the wallet has to be saved after every block to record where
|
|
||||||
// it got up to and for updating the transaction confidence data, which can slow down block chain download a lot.
|
|
||||||
// So this thread not only puts the work of saving onto a background thread but also coalesces requests together.
|
|
||||||
private static class AutosaveThread extends Thread {
|
|
||||||
private static DelayQueue<AutosaveThread.WalletSaveRequest> walletRefs = new DelayQueue<WalletSaveRequest>();
|
|
||||||
private static AutosaveThread globalThread;
|
|
||||||
|
|
||||||
private AutosaveThread() {
|
|
||||||
// Allow the JVM to shut down without waiting for this thread. Note this means users could lose auto-saves
|
|
||||||
// if they don't explicitly save the wallet before terminating!
|
|
||||||
setDaemon(true);
|
|
||||||
setName("Wallet auto save thread");
|
|
||||||
setPriority(Thread.MIN_PRIORITY); // Avoid competing with the UI.
|
|
||||||
Thread.UncaughtExceptionHandler handler = Threading.uncaughtExceptionHandler;
|
|
||||||
if (handler != null)
|
|
||||||
setUncaughtExceptionHandler(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the global instance that services all wallets. It never shuts down. */
|
|
||||||
public static void maybeStart() {
|
|
||||||
if (walletRefs.size() == 0) return;
|
|
||||||
|
|
||||||
synchronized (AutosaveThread.class) {
|
|
||||||
if (globalThread == null) {
|
|
||||||
globalThread = new AutosaveThread();
|
|
||||||
globalThread.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Called by a wallet when it's become dirty (changed). Will start the background thread if needed. */
|
|
||||||
public static void registerForSave(Wallet wallet, long delayMsec) {
|
|
||||||
walletRefs.add(new WalletSaveRequest(wallet, delayMsec));
|
|
||||||
maybeStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run() {
|
|
||||||
log.info("Auto-save thread starting up");
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
WalletSaveRequest req = walletRefs.poll(5, TimeUnit.SECONDS);
|
|
||||||
if (req == null) {
|
|
||||||
if (walletRefs.size() == 0) {
|
|
||||||
// No work to do for the given delay period, so let's shut down and free up memory.
|
|
||||||
// We'll get started up again if a wallet changes once more.
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
// There's work but nothing to do just yet. Go back to sleep and try again.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req.wallet.lock.lock();
|
|
||||||
try {
|
|
||||||
if (req.wallet.dirty) {
|
|
||||||
if (req.wallet.autoSave()) {
|
|
||||||
// Something went wrong, abort!
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
req.wallet.lock.unlock();
|
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.error("Auto-save thread interrupted during wait", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("Auto-save thread shutting down");
|
|
||||||
synchronized (AutosaveThread.class) {
|
|
||||||
Preconditions.checkState(globalThread == this); // There should only be one global thread.
|
|
||||||
globalThread = null;
|
|
||||||
}
|
|
||||||
// There's a possible shutdown race where work is added after we decided to shutdown but before
|
|
||||||
// we cleared globalThread.
|
|
||||||
maybeStart();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class WalletSaveRequest implements Delayed {
|
|
||||||
public final Wallet wallet;
|
|
||||||
public final long startTimeMs, requestedDelayMs;
|
|
||||||
|
|
||||||
public WalletSaveRequest(Wallet wallet, long requestedDelayMs) {
|
|
||||||
this.startTimeMs = System.currentTimeMillis();
|
|
||||||
this.requestedDelayMs = requestedDelayMs;
|
|
||||||
this.wallet = wallet;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getDelay(TimeUnit timeUnit) {
|
|
||||||
long delayRemainingMs = requestedDelayMs - (System.currentTimeMillis() - startTimeMs);
|
|
||||||
return timeUnit.convert(delayRemainingMs, TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int compareTo(Delayed delayed) {
|
|
||||||
if (delayed == this) return 0;
|
|
||||||
long delta = getDelay(TimeUnit.MILLISECONDS) - delayed.getDelay(TimeUnit.MILLISECONDS);
|
|
||||||
return (delta > 0 ? 1 : (delta < 0 ? -1 : 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object obj) {
|
|
||||||
if (!(obj instanceof WalletSaveRequest)) return false;
|
|
||||||
WalletSaveRequest w = (WalletSaveRequest) obj;
|
|
||||||
return w.startTimeMs == startTimeMs &&
|
|
||||||
w.requestedDelayMs == requestedDelayMs &&
|
|
||||||
w.wallet == wallet;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return Objects.hashCode(wallet, startTimeMs, requestedDelayMs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns true if the auto-save thread should abort */
|
|
||||||
private boolean autoSave() {
|
|
||||||
lock.lock();
|
|
||||||
final Sha256Hash lastBlockSeenHash = this.lastBlockSeenHash;
|
|
||||||
final AutosaveEventListener autosaveEventListener = this.autosaveEventListener;
|
|
||||||
final File autosaveToFile = this.autosaveToFile;
|
|
||||||
lock.unlock();
|
|
||||||
try {
|
|
||||||
log.info("Auto-saving wallet, last seen block is {}", lastBlockSeenHash);
|
|
||||||
File directory = autosaveToFile.getAbsoluteFile().getParentFile();
|
|
||||||
File temp = File.createTempFile("wallet", null, directory);
|
|
||||||
if (autosaveEventListener != null)
|
|
||||||
autosaveEventListener.onBeforeAutoSave(temp);
|
|
||||||
// This will clear the dirty flag.
|
|
||||||
saveToFile(temp, autosaveToFile);
|
|
||||||
if (autosaveEventListener != null)
|
|
||||||
autosaveEventListener.onAfterAutoSave(autosaveToFile);
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (autosaveEventListener != null && autosaveEventListener.caughtException(e))
|
|
||||||
return true;
|
|
||||||
else
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementors can handle exceptions thrown during wallet auto-save, and to do pre/post treatment of the wallet.
|
|
||||||
*/
|
|
||||||
public interface AutosaveEventListener {
|
|
||||||
/**
|
|
||||||
* Called on the auto-save thread if an exception is caught whilst saving the wallet.
|
|
||||||
* @return if true, terminates the auto-save thread. Otherwise sleeps and then tries again.
|
|
||||||
*/
|
|
||||||
public boolean caughtException(Throwable t);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called on the auto-save thread when a new temporary file is created but before the wallet data is saved
|
|
||||||
* to it. If you want to do something here like adjust permissions, go ahead and do so. The wallet is locked
|
|
||||||
* whilst this method is run.
|
|
||||||
*/
|
|
||||||
public void onBeforeAutoSave(File tempFile);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called on the auto-save thread after the newly created temporary file has been filled with data and renamed.
|
|
||||||
* The wallet is locked whilst this method is run.
|
|
||||||
*/
|
|
||||||
public void onAfterAutoSave(File newlySavedFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>Sets up the wallet to auto-save itself to the given file, using temp files with atomic renames to ensure
|
* <p>Sets up the wallet to auto-save itself to the given file, using temp files with atomic renames to ensure
|
||||||
* consistency. After connecting to a file, you no longer need to save the wallet manually, it will do it
|
* consistency. After connecting to a file, you no longer need to save the wallet manually, it will do it
|
||||||
@ -635,39 +453,40 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
* @param timeUnit the unit of measurement for delayTime.
|
* @param timeUnit the unit of measurement for delayTime.
|
||||||
* @param eventListener callback to be informed when the auto-save thread does things, or null
|
* @param eventListener callback to be informed when the auto-save thread does things, or null
|
||||||
*/
|
*/
|
||||||
public void autosaveToFile(File f, long delayTime, TimeUnit timeUnit,
|
public WalletFiles autosaveToFile(File f, long delayTime, TimeUnit timeUnit,
|
||||||
AutosaveEventListener eventListener) {
|
WalletFiles.Listener eventListener) {
|
||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
Preconditions.checkArgument(delayTime >= 0);
|
checkState(vFileManager == null, "Already auto saving this wallet.");
|
||||||
autosaveToFile = Preconditions.checkNotNull(f);
|
WalletFiles manager = new WalletFiles(this, f, delayTime, timeUnit);
|
||||||
autosaveEventListener = eventListener;
|
if (eventListener != null)
|
||||||
if (delayTime > 0) {
|
manager.setListener(eventListener);
|
||||||
autosaveDelayMs = TimeUnit.MILLISECONDS.convert(delayTime, timeUnit);
|
vFileManager = manager;
|
||||||
}
|
return manager;
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void queueAutoSave() {
|
private void saveLater() {
|
||||||
lock.lock();
|
WalletFiles files = vFileManager;
|
||||||
try {
|
if (files != null)
|
||||||
if (this.autosaveToFile == null) return;
|
files.saveLater();
|
||||||
if (autosaveDelayMs == 0) {
|
}
|
||||||
// No delay time was specified, so save now.
|
|
||||||
autoSave();
|
private void saveNow() {
|
||||||
} else {
|
// If auto saving is enabled, do an immediate sync write to disk ignoring any delays.
|
||||||
// If we need to, tell the auto save thread to wake us up. This will start the background thread if one
|
WalletFiles files = vFileManager;
|
||||||
// doesn't already exist. It will wake up once the delay expires and call autoSave().
|
if (files != null) {
|
||||||
// The background thread is shared between all wallets.
|
try {
|
||||||
if (!dirty) {
|
files.saveNow(); // This calls back into saveToFile().
|
||||||
dirty = true;
|
} catch (IOException e) {
|
||||||
AutosaveThread.registerForSave(this, autosaveDelayMs);
|
// Can't really do much at this point, just let the API user know.
|
||||||
}
|
log.error("Failed to save wallet to disk!", e);
|
||||||
|
Thread.UncaughtExceptionHandler handler = Threading.uncaughtExceptionHandler;
|
||||||
|
if (handler != null)
|
||||||
|
handler.uncaughtException(Thread.currentThread(), e);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
lock.unlock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -832,7 +651,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
* called to decide whether the wallet cares about the transaction - if it does, then this method expects the
|
* called to decide whether the wallet cares about the transaction - if it does, then this method expects the
|
||||||
* transaction and any dependencies it has which are still in the memory pool.</p>
|
* transaction and any dependencies it has which are still in the memory pool.</p>
|
||||||
*/
|
*/
|
||||||
public void receivePending(Transaction tx, List<Transaction> dependencies) throws VerificationException {
|
public void receivePending(Transaction tx, @Nullable List<Transaction> dependencies) throws VerificationException {
|
||||||
// Can run in a peer thread. This method will only be called if a prior call to isPendingTransactionRelevant
|
// Can run in a peer thread. This method will only be called if a prior call to isPendingTransactionRelevant
|
||||||
// returned true, so we already know by this point that it sends coins to or from our wallet, or is a double
|
// returned true, so we already know by this point that it sends coins to or from our wallet, or is a double
|
||||||
// spend against one of our other pending transactions.
|
// spend against one of our other pending transactions.
|
||||||
@ -1106,7 +925,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
|
|
||||||
informConfidenceListenersIfNotReorganizing();
|
informConfidenceListenersIfNotReorganizing();
|
||||||
checkState(isConsistent());
|
checkState(isConsistent());
|
||||||
queueAutoSave();
|
saveNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void informConfidenceListenersIfNotReorganizing() {
|
private void informConfidenceListenersIfNotReorganizing() {
|
||||||
@ -1156,7 +975,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
|
|
||||||
informConfidenceListenersIfNotReorganizing();
|
informConfidenceListenersIfNotReorganizing();
|
||||||
maybeQueueOnWalletChanged();
|
maybeQueueOnWalletChanged();
|
||||||
queueAutoSave();
|
// Coalesce writes to avoid throttling on disk access when catching up with the chain.
|
||||||
|
saveLater();
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@ -1425,7 +1245,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
|
|
||||||
checkState(isConsistent());
|
checkState(isConsistent());
|
||||||
informConfidenceListenersIfNotReorganizing();
|
informConfidenceListenersIfNotReorganizing();
|
||||||
queueAutoSave();
|
saveNow();
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@ -1611,7 +1431,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
spent.clear();
|
spent.clear();
|
||||||
pending.clear();
|
pending.clear();
|
||||||
dead.clear();
|
dead.clear();
|
||||||
queueAutoSave();
|
saveLater();
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
@ -2089,7 +1909,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given ECKey to the wallet. There is currently no way to delete keys (that would result in coin loss).
|
* Adds the given ECKey to the wallet. There is currently no way to delete keys (that would result in coin loss).
|
||||||
* If {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.bitcoin.core.Wallet.AutosaveEventListener)}
|
* If {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.bitcoin.wallet.WalletFiles.Listener)}
|
||||||
* has been called, triggers an auto save bypassing the normal coalescing delay and event handlers.
|
* has been called, triggers an auto save bypassing the normal coalescing delay and event handlers.
|
||||||
* If the key already exists in the wallet, does nothing and returns false.
|
* If the key already exists in the wallet, does nothing and returns false.
|
||||||
*/
|
*/
|
||||||
@ -2099,7 +1919,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given keys to the wallet. There is currently no way to delete keys (that would result in coin loss).
|
* Adds the given keys to the wallet. There is currently no way to delete keys (that would result in coin loss).
|
||||||
* If {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.bitcoin.core.Wallet.AutosaveEventListener)}
|
* If {@link Wallet#autosaveToFile(java.io.File, long, java.util.concurrent.TimeUnit, com.google.bitcoin.wallet.WalletFiles.Listener)}
|
||||||
* has been called, triggers an auto save bypassing the normal coalescing delay and event handlers.
|
* has been called, triggers an auto save bypassing the normal coalescing delay and event handlers.
|
||||||
* Returns the number of keys added, after duplicates are ignored. The onKeyAdded event will be called for each key
|
* Returns the number of keys added, after duplicates are ignored. The onKeyAdded event will be called for each key
|
||||||
* in the list that was not already present.
|
* in the list that was not already present.
|
||||||
@ -2124,9 +1944,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
}
|
}
|
||||||
queueOnKeysAdded(keys);
|
queueOnKeysAdded(keys);
|
||||||
// Force an auto-save immediately rather than queueing one, as keys are too important to risk losing.
|
// Force an auto-save immediately rather than queueing one, as keys are too important to risk losing.
|
||||||
if (autosaveToFile != null) {
|
saveNow();
|
||||||
autoSave();
|
|
||||||
}
|
|
||||||
return added;
|
return added;
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@ -2487,7 +2305,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
maybeQueueOnWalletChanged();
|
maybeQueueOnWalletChanged();
|
||||||
checkBalanceFuturesLocked(balance);
|
checkBalanceFuturesLocked(balance);
|
||||||
informConfidenceListenersIfNotReorganizing();
|
informConfidenceListenersIfNotReorganizing();
|
||||||
queueAutoSave();
|
saveLater();
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@ -2653,9 +2471,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
// The wallet is now encrypted.
|
// The wallet is now encrypted.
|
||||||
this.keyCrypter = keyCrypter;
|
this.keyCrypter = keyCrypter;
|
||||||
|
|
||||||
if (autosaveToFile != null) {
|
saveNow();
|
||||||
autoSave();
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@ -2695,10 +2511,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
|
|
||||||
// The wallet is now unencrypted.
|
// The wallet is now unencrypted.
|
||||||
keyCrypter = null;
|
keyCrypter = null;
|
||||||
|
saveNow();
|
||||||
if (autosaveToFile != null) {
|
|
||||||
autoSave();
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@ -3066,7 +2879,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
if (extensions.containsKey(id))
|
if (extensions.containsKey(id))
|
||||||
throw new IllegalStateException("Cannot add two extensions with the same ID: " + id);
|
throw new IllegalStateException("Cannot add two extensions with the same ID: " + id);
|
||||||
extensions.put(id, extension);
|
extensions.put(id, extension);
|
||||||
queueAutoSave();
|
saveNow();
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
@ -3083,7 +2896,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
if (previousExtension != null)
|
if (previousExtension != null)
|
||||||
return previousExtension;
|
return previousExtension;
|
||||||
extensions.put(id, extension);
|
extensions.put(id, extension);
|
||||||
queueAutoSave();
|
saveNow();
|
||||||
return extension;
|
return extension;
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
@ -3100,7 +2913,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
|
|||||||
lock.lock();
|
lock.lock();
|
||||||
try {
|
try {
|
||||||
extensions.put(id, extension);
|
extensions.put(id, extension);
|
||||||
queueAutoSave();
|
saveNow();
|
||||||
} finally {
|
} finally {
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
}
|
}
|
||||||
|
133
core/src/main/java/com/google/bitcoin/wallet/WalletFiles.java
Normal file
133
core/src/main/java/com/google/bitcoin/wallet/WalletFiles.java
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2013 Google Inc.
|
||||||
|
*
|
||||||
|
* 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 com.google.bitcoin.wallet;
|
||||||
|
|
||||||
|
import com.google.bitcoin.core.Wallet;
|
||||||
|
import com.google.bitcoin.utils.Threading;
|
||||||
|
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that handles atomic and optionally delayed writing of the wallet file to disk. In future: backups too.
|
||||||
|
* It can be useful to delay writing of a wallet file to disk on slow devices where disk and serialization overhead
|
||||||
|
* can come to dominate the chain processing speed, i.e. on Android phones. By coalescing writes and doing serialization
|
||||||
|
* and disk IO on a background thread performance can be improved.
|
||||||
|
*/
|
||||||
|
public class WalletFiles {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(WalletFiles.class);
|
||||||
|
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final ScheduledExecutorService executor;
|
||||||
|
private final File file;
|
||||||
|
private final AtomicBoolean savePending;
|
||||||
|
private final long delay;
|
||||||
|
private final TimeUnit delayTimeUnit;
|
||||||
|
private final Callable<Void> saver;
|
||||||
|
|
||||||
|
private volatile Listener vListener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementors can do pre/post treatment of the wallet file. Useful for adjusting permissions and other things.
|
||||||
|
*/
|
||||||
|
public interface Listener {
|
||||||
|
/**
|
||||||
|
* Called on the auto-save thread when a new temporary file is created but before the wallet data is saved
|
||||||
|
* to it. If you want to do something here like adjust permissions, go ahead and do so.
|
||||||
|
*/
|
||||||
|
public void onBeforeAutoSave(File tempFile);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on the auto-save thread after the newly created temporary file has been filled with data and renamed.
|
||||||
|
*/
|
||||||
|
public void onAfterAutoSave(File newlySavedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public WalletFiles(final Wallet wallet, File file, long delay, TimeUnit delayTimeUnit) {
|
||||||
|
final ThreadFactoryBuilder builder = new ThreadFactoryBuilder()
|
||||||
|
.setDaemon(true)
|
||||||
|
.setNameFormat("Wallet autosave thread")
|
||||||
|
.setPriority(Thread.MIN_PRIORITY); // Avoid competing with the GUI thread.
|
||||||
|
Thread.UncaughtExceptionHandler handler = Threading.uncaughtExceptionHandler;
|
||||||
|
if (handler != null)
|
||||||
|
builder.setUncaughtExceptionHandler(handler);
|
||||||
|
this.executor = Executors.newSingleThreadScheduledExecutor(builder.build());
|
||||||
|
this.wallet = checkNotNull(wallet);
|
||||||
|
// File must only be accessed from the auto-save executor from now on, to avoid simultaneous access.
|
||||||
|
this.file = checkNotNull(file);
|
||||||
|
this.savePending = new AtomicBoolean();
|
||||||
|
this.delay = delay;
|
||||||
|
this.delayTimeUnit = checkNotNull(delayTimeUnit);
|
||||||
|
|
||||||
|
this.saver = new Callable<Void>() {
|
||||||
|
@Override public Void call() throws Exception {
|
||||||
|
// Runs in an auto save thread.
|
||||||
|
if (!savePending.getAndSet(false)) {
|
||||||
|
// Some other scheduled request already beat us to it.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
log.info("Background saving wallet, last seen block is {}/{}", wallet.getLastBlockSeenHeight(), wallet.getLastBlockSeenHash());
|
||||||
|
saveNowInternal();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The given listener will be called on the autosave thread before and after the wallet is saved to disk.
|
||||||
|
*/
|
||||||
|
public void setListener(@Nonnull Listener listener) {
|
||||||
|
this.vListener = checkNotNull(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Actually write the wallet file to disk, using an atomic rename when possible. Runs on the current thread. */
|
||||||
|
public void saveNow() throws IOException {
|
||||||
|
// Can be called by any thread. However the wallet is locked whilst saving, so we can have two saves in flight
|
||||||
|
// but they will serialize (using different temp files).
|
||||||
|
log.info("Saving wallet, last seen block is {}/{}", wallet.getLastBlockSeenHeight(), wallet.getLastBlockSeenHash());
|
||||||
|
saveNowInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveNowInternal() throws IOException {
|
||||||
|
File directory = file.getAbsoluteFile().getParentFile();
|
||||||
|
File temp = File.createTempFile("wallet", null, directory);
|
||||||
|
final Listener listener = vListener;
|
||||||
|
if (listener != null)
|
||||||
|
listener.onBeforeAutoSave(temp);
|
||||||
|
wallet.saveToFile(temp, file);
|
||||||
|
if (listener != null)
|
||||||
|
listener.onAfterAutoSave(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Queues up a save in the background. Useful for not very important wallet changes. */
|
||||||
|
public void saveLater() {
|
||||||
|
if (savePending.getAndSet(true))
|
||||||
|
return; // Already pending.
|
||||||
|
executor.schedule(saver, delay, delayTimeUnit);
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@ import com.google.bitcoin.crypto.KeyCrypter;
|
|||||||
import com.google.bitcoin.crypto.KeyCrypterException;
|
import com.google.bitcoin.crypto.KeyCrypterException;
|
||||||
import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
|
import com.google.bitcoin.wallet.WalletFiles;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
@ -925,15 +926,11 @@ public class WalletTest extends TestWithWallet {
|
|||||||
// updates are coalesced together. This test is a bit racy, it assumes we can complete the unit test within
|
// updates are coalesced together. This test is a bit racy, it assumes we can complete the unit test within
|
||||||
// an auto-save cycle of 1 second.
|
// an auto-save cycle of 1 second.
|
||||||
final File[] results = new File[2];
|
final File[] results = new File[2];
|
||||||
final CountDownLatch latch = new CountDownLatch(2);
|
final CountDownLatch latch = new CountDownLatch(3);
|
||||||
File f = File.createTempFile("bitcoinj-unit-test", null);
|
File f = File.createTempFile("bitcoinj-unit-test", null);
|
||||||
Sha256Hash hash1 = Sha256Hash.hashFileContents(f);
|
Sha256Hash hash1 = Sha256Hash.hashFileContents(f);
|
||||||
wallet.autosaveToFile(f, 1, TimeUnit.SECONDS,
|
wallet.autosaveToFile(f, 1, TimeUnit.SECONDS,
|
||||||
new Wallet.AutosaveEventListener() {
|
new WalletFiles.Listener() {
|
||||||
public boolean caughtException(Throwable t) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onBeforeAutoSave(File tempFile) {
|
public void onBeforeAutoSave(File tempFile) {
|
||||||
results[0] = tempFile;
|
results[0] = tempFile;
|
||||||
}
|
}
|
||||||
@ -952,24 +949,24 @@ public class WalletTest extends TestWithWallet {
|
|||||||
assertEquals(f, results[1]);
|
assertEquals(f, results[1]);
|
||||||
results[0] = results[1] = null;
|
results[0] = results[1] = null;
|
||||||
|
|
||||||
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
|
Block b0 = createFakeBlock(blockStore).block;
|
||||||
if (wallet.isPendingTransactionRelevant(t1))
|
chain.add(b0);
|
||||||
wallet.receivePending(t1, null);
|
|
||||||
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
|
Sha256Hash hash3 = Sha256Hash.hashFileContents(f);
|
||||||
assertTrue(hash2.equals(hash3)); // File has NOT changed.
|
assertEquals(hash2, hash3); // File has NOT changed yet. Just new blocks with no txns - delayed.
|
||||||
assertNull(results[0]);
|
assertNull(results[0]);
|
||||||
assertNull(results[1]);
|
assertNull(results[1]);
|
||||||
|
|
||||||
|
Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
|
||||||
Block b1 = createFakeBlock(blockStore, t1).block;
|
Block b1 = createFakeBlock(blockStore, t1).block;
|
||||||
chain.add(b1);
|
chain.add(b1);
|
||||||
Sha256Hash hash4 = Sha256Hash.hashFileContents(f);
|
Sha256Hash hash4 = Sha256Hash.hashFileContents(f);
|
||||||
assertTrue(hash3.equals(hash4)); // File has NOT changed.
|
assertFalse(hash3.equals(hash4)); // File HAS changed.
|
||||||
assertNull(results[0]);
|
results[0] = results[1] = null;
|
||||||
assertNull(results[1]);
|
|
||||||
|
|
||||||
|
// A block that contains some random tx we don't care about.
|
||||||
Block b2 = b1.createNextBlock(new ECKey().toAddress(params));
|
Block b2 = b1.createNextBlock(new ECKey().toAddress(params));
|
||||||
chain.add(b2);
|
chain.add(b2);
|
||||||
assertTrue(hash4.equals(Sha256Hash.hashFileContents(f))); // File has NOT changed.
|
assertEquals(hash4, Sha256Hash.hashFileContents(f)); // File has NOT changed.
|
||||||
assertNull(results[0]);
|
assertNull(results[0]);
|
||||||
assertNull(results[1]);
|
assertNull(results[1]);
|
||||||
|
|
||||||
|
@ -17,12 +17,8 @@
|
|||||||
package com.google.bitcoin.protocols.channels;
|
package com.google.bitcoin.protocols.channels;
|
||||||
|
|
||||||
import com.google.bitcoin.core.*;
|
import com.google.bitcoin.core.*;
|
||||||
import com.google.bitcoin.protocols.niowrapper.ProtobufParser;
|
|
||||||
import com.google.bitcoin.protocols.niowrapper.ProtobufParserFactory;
|
|
||||||
import com.google.bitcoin.protocols.niowrapper.ProtobufServer;
|
|
||||||
import com.google.bitcoin.utils.Threading;
|
import com.google.bitcoin.utils.Threading;
|
||||||
import org.bitcoin.paymentchannel.Protos;
|
import com.google.bitcoin.wallet.WalletFiles;
|
||||||
import org.junit.Before;
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.SettableFuture;
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
@ -143,14 +139,7 @@ public class ChannelConnectionTest extends TestWithWallet {
|
|||||||
final CountDownLatch latch = new CountDownLatch(3); // Expect 3 calls.
|
final CountDownLatch latch = new CountDownLatch(3); // Expect 3 calls.
|
||||||
File tempFile = File.createTempFile("channel_connection_test", ".wallet");
|
File tempFile = File.createTempFile("channel_connection_test", ".wallet");
|
||||||
tempFile.deleteOnExit();
|
tempFile.deleteOnExit();
|
||||||
serverWallet.autosaveToFile(tempFile, 0, TimeUnit.SECONDS, new Wallet.AutosaveEventListener() {
|
serverWallet.autosaveToFile(tempFile, 0, TimeUnit.SECONDS, new WalletFiles.Listener() {
|
||||||
@Override
|
|
||||||
public boolean caughtException(Throwable t) {
|
|
||||||
t.printStackTrace();
|
|
||||||
System.exit(-1);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBeforeAutoSave(File tempFile) {
|
public void onBeforeAutoSave(File tempFile) {
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
|
Loading…
Reference in New Issue
Block a user