3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-12 10:15:52 +00:00

Another pass at the auto-save code. Resolves issue 246.

This commit is contained in:
Mike Hearn 2012-08-27 18:44:35 +02:00
parent 4e708ed4b0
commit 56d74d504a
2 changed files with 41 additions and 31 deletions

View File

@ -254,37 +254,40 @@ public class Wallet implements Serializable {
saveToFile(temp, f);
}
// This must be a static class to avoid creating a strong reference from this thread to the wallet. If we did that
// it would never become garbage collectable because the autosave thread would never die. To avoid this from
// happening the wallet posts a this pointer to us when it wants to get saved and we pick it up a short time
// later. The delay allows coalescion of many rapid updates into occasional saves, and it means disk IO can be
// done on a different thread which helps make UIs more responsive.
// 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 DelayQueue<AutosaveThread.WalletSaveRequest> walletRefs;
private static DelayQueue<AutosaveThread.WalletSaveRequest> walletRefs = new DelayQueue<WalletSaveRequest>();
private static AutosaveThread globalThread;
private AutosaveThread() {
this.walletRefs = new DelayQueue<WalletSaveRequest>();
setDaemon(true); // Allow the JVM to exit even if this thread is still running.
// 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.
}
/** Returns the global instance that services all wallets. It never shuts down. */
public static AutosaveThread get() {
public static void maybeStart() {
if (walletRefs.size() == 0) return;
synchronized (AutosaveThread.class) {
if (globalThread != null)
return globalThread;
globalThread = new AutosaveThread();
globalThread.start();
return globalThread;
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) {
AutosaveThread ats = get();
ats.walletRefs.put(new WalletSaveRequest(wallet, delayMsec));
walletRefs.add(new WalletSaveRequest(wallet, delayMsec));
maybeStart();
}
public void run() {
@ -292,10 +295,15 @@ public class Wallet implements Serializable {
while (true) {
try {
WalletSaveRequest req = walletRefs.poll(5, TimeUnit.SECONDS);
if (req == null && 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 again.
break;
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;
}
}
synchronized (req.wallet) {
if (req.wallet.dirty) {
@ -306,6 +314,7 @@ public class Wallet implements Serializable {
}
}
} catch (InterruptedException e) {
log.error("Auto-save thread interrupted during wait", e);
break;
}
}
@ -314,6 +323,9 @@ public class Wallet implements Serializable {
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 {
@ -341,6 +353,9 @@ public class Wallet implements Serializable {
/** Returns true if the auto-save thread should abort */
private synchronized boolean autoSave() {
// TODO: This code holds the wallet lock for much longer than actually necessary.
// It only actually needs to be held whilst converting the wallet to in-memory protobuf objects. The act
// of writing out to disk, renaming, etc, only needs the lock when accessing data members.
try {
log.info("Auto-saving wallet, last seen block is {}", lastBlockSeenHash);
File directory = autosaveToFile.getAbsoluteFile().getParentFile();
@ -389,19 +404,14 @@ public class Wallet implements Serializable {
* <p>If delayTime is set, a background thread will be created and the wallet will only be saved to
* disk every so many time units. If no changes have occurred for the given time period, nothing will be written.
* In this way disk IO can be rate limited. It's a good idea to set this as otherwise the wallet can change very
* frequently, eg if there are a lot of transactions in it or a busy key, and there will be a lot of redundant
* frequently, eg if there are a lot of transactions in it or during block sync, and there will be a lot of redundant
* writes. Note that when a new key is added, that always results in an immediate save regardless of
* delayTime.</p>
* delayTime. <b>You should still save the wallet manually when your program is about to shut down as the JVM
* will not wait for the background thread.</b></p>
*
* <p>An event listener can be provided. If a delay >0 was specified, it will be called on a background thread
* with the wallet locked when an auto-save occurs. If delay is zero or you do something that always triggers
* an immediate save, like adding a key, the event listener will be invoked on the calling threads. There is
* an important detail to get right here. The background thread that performs auto-saving keeps a weak reference
* to the wallet and shuts itself down if the wallet is garbage collected, so you don't have to think about it.
* If you provide an event listener however, it'd be very easy to accidentally hold a strong reference from your
* event listener to the wallet, meaning that the background thread will transitively keep your wallet object
* alive and un-collectable. So be careful to use a static inner class for this to avoid that problem, unless
* you don't care about keeping wallets alive indefinitely.</p>
* an immediate save, like adding a key, the event listener will be invoked on the calling threads.</p>
*
* @param f The destination file to save to.
* @param delayTime How many time units to wait until saving the wallet on a background thread.

View File

@ -339,9 +339,8 @@ public class WalletTool {
return;
}
saveWallet(walletFile);
} else {
shutdown();
}
shutdown();
}
private static void send(List<String> outputs) {
@ -538,6 +537,7 @@ public class WalletTool {
peers.stop();
saveWallet(walletFile);
store.close();
wallet = null;
} catch (BlockStoreException e) {
throw new RuntimeException(e);
}