();
- 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);
- }
-
/**
* 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
@@ -635,39 +453,40 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
* @param timeUnit the unit of measurement for delayTime.
* @param eventListener callback to be informed when the auto-save thread does things, or null
*/
- public void autosaveToFile(File f, long delayTime, TimeUnit timeUnit,
- AutosaveEventListener eventListener) {
+ public WalletFiles autosaveToFile(File f, long delayTime, TimeUnit timeUnit,
+ WalletFiles.Listener eventListener) {
lock.lock();
try {
- Preconditions.checkArgument(delayTime >= 0);
- autosaveToFile = Preconditions.checkNotNull(f);
- autosaveEventListener = eventListener;
- if (delayTime > 0) {
- autosaveDelayMs = TimeUnit.MILLISECONDS.convert(delayTime, timeUnit);
- }
+ checkState(vFileManager == null, "Already auto saving this wallet.");
+ WalletFiles manager = new WalletFiles(this, f, delayTime, timeUnit);
+ if (eventListener != null)
+ manager.setListener(eventListener);
+ vFileManager = manager;
+ return manager;
} finally {
lock.unlock();
}
}
- private void queueAutoSave() {
- lock.lock();
- try {
- if (this.autosaveToFile == null) return;
- if (autosaveDelayMs == 0) {
- // No delay time was specified, so save now.
- autoSave();
- } else {
- // If we need to, tell the auto save thread to wake us up. This will start the background thread if one
- // doesn't already exist. It will wake up once the delay expires and call autoSave().
- // The background thread is shared between all wallets.
- if (!dirty) {
- dirty = true;
- AutosaveThread.registerForSave(this, autosaveDelayMs);
- }
+ private void saveLater() {
+ WalletFiles files = vFileManager;
+ if (files != null)
+ files.saveLater();
+ }
+
+ private void saveNow() {
+ // If auto saving is enabled, do an immediate sync write to disk ignoring any delays.
+ WalletFiles files = vFileManager;
+ if (files != null) {
+ try {
+ files.saveNow(); // This calls back into saveToFile().
+ } catch (IOException e) {
+ // 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
* transaction and any dependencies it has which are still in the memory pool.
*/
- public void receivePending(Transaction tx, List dependencies) throws VerificationException {
+ public void receivePending(Transaction tx, @Nullable List dependencies) throws VerificationException {
// 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
// spend against one of our other pending transactions.
@@ -1106,7 +925,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
informConfidenceListenersIfNotReorganizing();
checkState(isConsistent());
- queueAutoSave();
+ saveNow();
}
private void informConfidenceListenersIfNotReorganizing() {
@@ -1156,7 +975,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
informConfidenceListenersIfNotReorganizing();
maybeQueueOnWalletChanged();
- queueAutoSave();
+ // Coalesce writes to avoid throttling on disk access when catching up with the chain.
+ saveLater();
} finally {
lock.unlock();
}
@@ -1425,7 +1245,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
checkState(isConsistent());
informConfidenceListenersIfNotReorganizing();
- queueAutoSave();
+ saveNow();
} finally {
lock.unlock();
}
@@ -1611,7 +1431,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
spent.clear();
pending.clear();
dead.clear();
- queueAutoSave();
+ saveLater();
} else {
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).
- * 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.
* 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).
- * 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.
* 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.
@@ -2124,9 +1944,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
}
queueOnKeysAdded(keys);
// Force an auto-save immediately rather than queueing one, as keys are too important to risk losing.
- if (autosaveToFile != null) {
- autoSave();
- }
+ saveNow();
return added;
} finally {
lock.unlock();
@@ -2487,7 +2305,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
maybeQueueOnWalletChanged();
checkBalanceFuturesLocked(balance);
informConfidenceListenersIfNotReorganizing();
- queueAutoSave();
+ saveLater();
} finally {
lock.unlock();
}
@@ -2653,9 +2471,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
// The wallet is now encrypted.
this.keyCrypter = keyCrypter;
- if (autosaveToFile != null) {
- autoSave();
- }
+ saveNow();
} finally {
lock.unlock();
}
@@ -2695,10 +2511,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
// The wallet is now unencrypted.
keyCrypter = null;
-
- if (autosaveToFile != null) {
- autoSave();
- }
+ saveNow();
} finally {
lock.unlock();
}
@@ -3066,7 +2879,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
if (extensions.containsKey(id))
throw new IllegalStateException("Cannot add two extensions with the same ID: " + id);
extensions.put(id, extension);
- queueAutoSave();
+ saveNow();
} finally {
lock.unlock();
}
@@ -3083,7 +2896,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
if (previousExtension != null)
return previousExtension;
extensions.put(id, extension);
- queueAutoSave();
+ saveNow();
return extension;
} finally {
lock.unlock();
@@ -3100,7 +2913,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi
lock.lock();
try {
extensions.put(id, extension);
- queueAutoSave();
+ saveNow();
} finally {
lock.unlock();
}
diff --git a/core/src/main/java/com/google/bitcoin/wallet/WalletFiles.java b/core/src/main/java/com/google/bitcoin/wallet/WalletFiles.java
new file mode 100644
index 00000000..e54793b0
--- /dev/null
+++ b/core/src/main/java/com/google/bitcoin/wallet/WalletFiles.java
@@ -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 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() {
+ @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);
+ }
+}
diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java
index 9ecd672e..5aaae972 100644
--- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java
+++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java
@@ -23,6 +23,7 @@ import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterException;
import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.utils.Threading;
+import com.google.bitcoin.wallet.WalletFiles;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ListenableFuture;
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
// an auto-save cycle of 1 second.
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);
Sha256Hash hash1 = Sha256Hash.hashFileContents(f);
wallet.autosaveToFile(f, 1, TimeUnit.SECONDS,
- new Wallet.AutosaveEventListener() {
- public boolean caughtException(Throwable t) {
- return false;
- }
-
+ new WalletFiles.Listener() {
public void onBeforeAutoSave(File tempFile) {
results[0] = tempFile;
}
@@ -952,24 +949,24 @@ public class WalletTest extends TestWithWallet {
assertEquals(f, results[1]);
results[0] = results[1] = null;
- Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
- if (wallet.isPendingTransactionRelevant(t1))
- wallet.receivePending(t1, null);
+ Block b0 = createFakeBlock(blockStore).block;
+ chain.add(b0);
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[1]);
+ Transaction t1 = createFakeTx(params, toNanoCoins(5, 0), key);
Block b1 = createFakeBlock(blockStore, t1).block;
chain.add(b1);
Sha256Hash hash4 = Sha256Hash.hashFileContents(f);
- assertTrue(hash3.equals(hash4)); // File has NOT changed.
- assertNull(results[0]);
- assertNull(results[1]);
+ assertFalse(hash3.equals(hash4)); // File HAS changed.
+ results[0] = results[1] = null;
+ // A block that contains some random tx we don't care about.
Block b2 = b1.createNextBlock(new ECKey().toAddress(params));
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[1]);
diff --git a/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java b/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java
index 9d0ab282..471e287c 100644
--- a/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java
+++ b/core/src/test/java/com/google/bitcoin/protocols/channels/ChannelConnectionTest.java
@@ -17,12 +17,8 @@
package com.google.bitcoin.protocols.channels;
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 org.bitcoin.paymentchannel.Protos;
-import org.junit.Before;
+import com.google.bitcoin.wallet.WalletFiles;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.google.protobuf.ByteString;
@@ -143,14 +139,7 @@ public class ChannelConnectionTest extends TestWithWallet {
final CountDownLatch latch = new CountDownLatch(3); // Expect 3 calls.
File tempFile = File.createTempFile("channel_connection_test", ".wallet");
tempFile.deleteOnExit();
- serverWallet.autosaveToFile(tempFile, 0, TimeUnit.SECONDS, new Wallet.AutosaveEventListener() {
- @Override
- public boolean caughtException(Throwable t) {
- t.printStackTrace();
- System.exit(-1);
- return false;
- }
-
+ serverWallet.autosaveToFile(tempFile, 0, TimeUnit.SECONDS, new WalletFiles.Listener() {
@Override
public void onBeforeAutoSave(File tempFile) {
latch.countDown();