3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-14 19:25:51 +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); 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 // Auto-saving can be done on a background thread if the user wishes it, this is to avoid stalling threads calling
// it would never become garbage collectable because the autosave thread would never die. To avoid this from // into the wallet on serialization/disk access all the time which is important in GUI apps where you don't want
// happening the wallet posts a this pointer to us when it wants to get saved and we pick it up a short time // the main thread to ever wait on disk (otherwise you lose a lot of responsiveness). The primary case where it
// later. The delay allows coalescion of many rapid updates into occasional saves, and it means disk IO can be // can be a problem is during block chain syncup - the wallet has to be saved after every block to record where
// done on a different thread which helps make UIs more responsive. // 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 class AutosaveThread extends Thread {
private DelayQueue<AutosaveThread.WalletSaveRequest> walletRefs; private static DelayQueue<AutosaveThread.WalletSaveRequest> walletRefs = new DelayQueue<WalletSaveRequest>();
private static AutosaveThread globalThread; private static AutosaveThread globalThread;
private AutosaveThread() { private AutosaveThread() {
this.walletRefs = new DelayQueue<WalletSaveRequest>(); // Allow the JVM to shut down without waiting for this thread. Note this means users could lose auto-saves
setDaemon(true); // Allow the JVM to exit even if this thread is still running. // if they don't explicitly save the wallet before terminating!
setDaemon(true);
setName("Wallet auto save thread"); setName("Wallet auto save thread");
setPriority(Thread.MIN_PRIORITY); // Avoid competing with the UI. setPriority(Thread.MIN_PRIORITY); // Avoid competing with the UI.
} }
/** Returns the global instance that services all wallets. It never shuts down. */ /** 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) { synchronized (AutosaveThread.class) {
if (globalThread != null) if (globalThread == null) {
return globalThread; globalThread = new AutosaveThread();
globalThread = new AutosaveThread(); globalThread.start();
globalThread.start(); }
return globalThread;
} }
} }
/** Called by a wallet when it's become dirty (changed). Will start the background thread if needed. */ /** 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) { public static void registerForSave(Wallet wallet, long delayMsec) {
AutosaveThread ats = get(); walletRefs.add(new WalletSaveRequest(wallet, delayMsec));
ats.walletRefs.put(new WalletSaveRequest(wallet, delayMsec)); maybeStart();
} }
public void run() { public void run() {
@ -292,10 +295,15 @@ public class Wallet implements Serializable {
while (true) { while (true) {
try { try {
WalletSaveRequest req = walletRefs.poll(5, TimeUnit.SECONDS); WalletSaveRequest req = walletRefs.poll(5, TimeUnit.SECONDS);
if (req == null && walletRefs.size() == 0) { if (req == null) {
// No work to do for the given delay period, so let's shut down and free up memory. We'll get if (walletRefs.size() == 0) {
// started up again if a wallet changes again. // No work to do for the given delay period, so let's shut down and free up memory.
break; // 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) { synchronized (req.wallet) {
if (req.wallet.dirty) { if (req.wallet.dirty) {
@ -306,6 +314,7 @@ public class Wallet implements Serializable {
} }
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error("Auto-save thread interrupted during wait", e);
break; break;
} }
} }
@ -314,6 +323,9 @@ public class Wallet implements Serializable {
Preconditions.checkState(globalThread == this); // There should only be one global thread. Preconditions.checkState(globalThread == this); // There should only be one global thread.
globalThread = null; 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 { private static class WalletSaveRequest implements Delayed {
@ -341,6 +353,9 @@ public class Wallet implements Serializable {
/** Returns true if the auto-save thread should abort */ /** Returns true if the auto-save thread should abort */
private synchronized boolean autoSave() { 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 { try {
log.info("Auto-saving wallet, last seen block is {}", lastBlockSeenHash); log.info("Auto-saving wallet, last seen block is {}", lastBlockSeenHash);
File directory = autosaveToFile.getAbsoluteFile().getParentFile(); 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 * <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. * 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 * 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 * 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 * <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 * 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 immediate save, like adding a key, the event listener will be invoked on the calling threads.</p>
* 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>
* *
* @param f The destination file to save to. * @param f The destination file to save to.
* @param delayTime How many time units to wait until saving the wallet on a background thread. * @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; return;
} }
saveWallet(walletFile); saveWallet(walletFile);
} else {
shutdown();
} }
shutdown();
} }
private static void send(List<String> outputs) { private static void send(List<String> outputs) {
@ -538,6 +537,7 @@ public class WalletTool {
peers.stop(); peers.stop();
saveWallet(walletFile); saveWallet(walletFile);
store.close(); store.close();
wallet = null;
} catch (BlockStoreException e) { } catch (BlockStoreException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }