save wallets after elapsed time and on wallet operations

This commit is contained in:
woodser 2025-09-24 03:10:21 -04:00
parent fb361730c7
commit 37d1b16893
No known key found for this signature in database
GPG key ID: 55A10DD48ADEE5EF
4 changed files with 63 additions and 51 deletions

View file

@ -167,7 +167,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
private Subscription protocolErrorStateSubscription;
private Subscription protocolErrorHeightSubscription;
public static final String PROTOCOL_VERSION = "protocolVersion"; // key for extraDataMap in trade statistics
public BooleanProperty wasWalletPolled = new SimpleBooleanProperty(false);
public BooleanProperty wasWalletPolledProperty = new SimpleBooleanProperty(false);
public BooleanProperty wasWalletSyncedAndPolledProperty = new SimpleBooleanProperty(false);
private static final long MISSING_TXS_DELAY_MS = Config.baseCurrencyNetwork().isTestnet() ? 5000 : 30000;
///////////////////////////////////////////////////////////////////////////////////////////
@ -838,16 +839,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
getProtocol().maybeReprocessPaymentReceivedMessage(false);
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false);
// handle when wallet first polled
if (wasWalletPolled.get()) onWalletFirstPolled();
// handle when wallet first synced
if (wasWalletSyncedAndPolledProperty.get()) onWalletFirstSynced();
else {
wasWalletPolled.addListener((observable, oldValue, newValue) -> {
if (newValue) onWalletFirstPolled();
wasWalletSyncedAndPolledProperty.addListener((observable, oldValue, newValue) -> {
if (newValue) onWalletFirstSynced();
});
}
}
private void onWalletFirstPolled() {
private void onWalletFirstSynced() {
requestSaveWallet();
checkForUnconfirmedTimeout();
}
@ -1000,14 +1001,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
@Override
public void requestSaveWallet() {
public void requestSaveWalletIfElapsedTime() {
ThreadUtils.submitToPool(() -> {
synchronized (walletLock) {
if (walletExists()) saveWalletIfElapsedTime();
}
});
}
// save wallet off main thread
ThreadUtils.execute(() -> {
private void requestSaveWallet() {
ThreadUtils.submitToPool(() -> {
synchronized (walletLock) {
if (walletExists()) saveWallet();
}
}, getId());
});
}
@Override
@ -1019,6 +1026,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getShortId());
xmrWalletService.saveWallet(wallet);
lastSaveTimeMs = System.currentTimeMillis();
maybeBackupWallet();
}
}
@ -1448,8 +1456,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} finally {
requestSaveWallet();
requestPersistence();
saveWallet();
persistNow(null);
}
}
}
@ -2394,7 +2402,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
public boolean isDepositTxMissing() {
if (!wasWalletPolled.get()) throw new IllegalStateException("Cannot determine if deposit tx is missing because wallet has not been polled");
if (!wasWalletPolledProperty.get()) throw new IllegalStateException("Cannot determine if deposit tx is missing because wallet has not been polled");
MoneroTxWallet makerDepositTx = getMakerDepositTx();
MoneroTxWallet takerDepositTx = getTakerDepositTx();
boolean hasUnlockedDepositTx = (makerDepositTx != null && Boolean.FALSE.equals(makerDepositTx.isLocked())) || (takerDepositTx != null && Boolean.FALSE.equals(takerDepositTx.isLocked()));
@ -2960,10 +2968,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
setPayoutTx(txs, checkPool);
}
// update trade period if applicable
// update trade period and poll properties
maybeUpdateTradePeriod();
wasWalletPolledProperty.set(true);
if (!offlinePoll) wasWalletSyncedAndPolledProperty.set(true);
} catch (Exception e) {
if (!(e instanceof IllegalStateException) && !isShutDownStarted && !wasWalletPolled.get()) { // request connection switch if failure on first poll
if (!(e instanceof IllegalStateException) && !isShutDownStarted && !offlinePoll && !wasWalletSyncedAndPolledProperty.get()) { // request connection switch on failure until synced and polled
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
}
if (HavenoUtils.isUnresponsive(e)) { // wallet can be stuck a while
@ -2984,8 +2994,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
pollInProgress = false;
}
}
wasWalletPolled.set(true);
saveWalletWithDelay();
requestSaveWalletIfElapsedTime();
}
}
@ -3364,7 +3373,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (!skipLog) log.info("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getShortId());
wallet.rescanSpent();
if (!skipLog) log.info("Done rescanning spent outputs for {} {}", getClass().getSimpleName(), getShortId());
saveWalletWithDelay();
saveWalletIfElapsedTime();
} catch (Exception e) {
log.warn("Error rescanning spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while

View file

@ -12,6 +12,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.core.api.XmrConnectionService;
@ -33,9 +34,11 @@ import monero.wallet.model.MoneroWalletListener;
public abstract class XmrWalletBase {
// constants
public static final int SYNC_TIMEOUT_SECONDS = 240;
public static final int SAVE_WALLET_DELAY_SECONDS = 300;
private static final String SYNC_PROGRESS_TIMEOUT_MSG = "Sync progress timeout called";
private static final int SYNC_TIMEOUT_SECONDS = 240;
private static final String SYNC_TIMEOUT_MSG = "Sync timeout called";
private static final long SAVE_AFTER_ELAPSED_SECONDS = 300;
private Object saveIntervalLock = new Object();
protected long lastSaveTimeMs = 0;
// inherited
protected MoneroWallet wallet;
@ -80,6 +83,7 @@ public abstract class XmrWalletBase {
Callable<MoneroSyncResult> task = () -> {
MoneroSyncResult result = wallet.sync();
saveWalletIfElapsedTime();
walletHeight.set(wallet.getHeight());
return result;
};
@ -90,7 +94,7 @@ public abstract class XmrWalletBase {
return future.get(timeoutSec, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
throw new RuntimeException("Sync timed out after " + timeoutSec + " seconds", e);
throw new RuntimeException(SYNC_TIMEOUT_MSG, e);
} catch (ExecutionException e) {
throw new RuntimeException("Sync failed", e.getCause());
} catch (InterruptedException e) {
@ -190,7 +194,7 @@ public abstract class XmrWalletBase {
if (wallet != null) { // can become null if interrupted by force close
if (syncProgressError == null || !HavenoUtils.isUnresponsive(syncProgressError)) { // TODO: skipping stop sync if unresponsive because wallet will hang. if unresponsive, wallet is assumed to be force restarted by caller, but that should be done internally here instead of externally?
wallet.stopSyncing();
saveWallet();
saveWalletIfElapsedTime();
}
}
if (syncProgressError != null) throw new RuntimeException(syncProgressError);
@ -210,26 +214,27 @@ public abstract class XmrWalletBase {
return false;
}
public void saveWalletWithDelay() {
// delay writing to disk to avoid frequent write operations
if (saveWalletDelayTimer == null) {
saveWalletDelayTimer = UserThread.runAfter(() -> {
requestSaveWallet();
UserThread.execute(() -> saveWalletDelayTimer = null);
}, SAVE_WALLET_DELAY_SECONDS, TimeUnit.SECONDS);
public void saveWalletIfElapsedTime() {
synchronized (saveIntervalLock) {
if (System.currentTimeMillis() - lastSaveTimeMs >= SAVE_AFTER_ELAPSED_SECONDS * 1000) {
saveWallet();
lastSaveTimeMs = System.currentTimeMillis();
}
}
}
public void requestSaveWalletIfElapsedTime() {
ThreadUtils.submitToPool(() -> saveWalletIfElapsedTime());
}
public static boolean isSyncWithProgressTimeout(Throwable e) {
return e.getMessage().contains(SYNC_TIMEOUT_MSG);
}
// --------------------------------- ABSTRACT -----------------------------
public static boolean isSyncWithProgressTimeout(Throwable e) {
return e.getMessage().contains(SYNC_PROGRESS_TIMEOUT_MSG);
}
public abstract void saveWallet();
public abstract void requestSaveWallet();
protected abstract void onConnectionChanged(MoneroRpcConnection connection);
// ------------------------------ PRIVATE HELPERS -------------------------
@ -261,7 +266,7 @@ public abstract class XmrWalletBase {
if (syncProgressTimeout != null) syncProgressTimeout.stop();
syncProgressTimeout = UserThread.runAfter(() -> {
if (isShutDownStarted) return;
syncProgressError = new RuntimeException(SYNC_PROGRESS_TIMEOUT_MSG);
syncProgressError = new RuntimeException(SYNC_TIMEOUT_MSG);
syncProgressLatch.countDown();
}, SYNC_TIMEOUT_SECONDS, TimeUnit.SECONDS);
}

View file

@ -239,7 +239,10 @@ public class XmrWalletService extends XmrWalletBase {
@Override
public void saveWallet() {
saveWallet(shouldBackup(wallet));
synchronized (walletLock) {
saveWallet(shouldBackup(wallet));
lastSaveTimeMs = System.currentTimeMillis();
}
}
private boolean shouldBackup(MoneroWallet wallet) {
@ -252,11 +255,6 @@ public class XmrWalletService extends XmrWalletBase {
}
}
@Override
public void requestSaveWallet() {
ThreadUtils.submitToPool(() -> saveWallet()); // save wallet off main thread
}
public boolean isWalletAvailable() {
try {
return getWallet() != null;
@ -432,7 +430,7 @@ public class XmrWalletService extends XmrWalletBase {
if (Boolean.TRUE.equals(txConfig.getRelay())) {
cachedTxs.addFirst(tx);
cacheWalletInfo();
requestSaveWallet();
saveWallet();
}
return tx;
}
@ -450,7 +448,7 @@ public class XmrWalletService extends XmrWalletBase {
if (Boolean.TRUE.equals(txConfig.getRelay())) {
for (MoneroTxWallet tx : txs) cachedTxs.addFirst(tx);
cacheWalletInfo();
requestSaveWallet();
saveWallet();
}
return txs;
}
@ -460,7 +458,7 @@ public class XmrWalletService extends XmrWalletBase {
public List<String> relayTxs(List<String> metadatas) {
synchronized (walletLock) {
List<String> txIds = wallet.relayTxs(metadatas);
requestSaveWallet();
saveWallet();
return txIds;
}
}
@ -554,7 +552,7 @@ public class XmrWalletService extends XmrWalletBase {
for (String keyImage : unfrozenKeyImages) wallet.freezeOutput(keyImage);
cacheNonPoolTxs();
cacheWalletInfo();
requestSaveWallet();
saveWallet();
}
}
@ -577,7 +575,7 @@ public class XmrWalletService extends XmrWalletBase {
for (String keyImage : frozenKeyImages) wallet.thawOutput(keyImage);
cacheNonPoolTxs();
cacheWalletInfo();
requestSaveWallet();
saveWallet();
}
}
@ -2075,7 +2073,7 @@ public class XmrWalletService extends XmrWalletBase {
pollInProgress = false;
}
}
saveWalletWithDelay();
saveWalletIfElapsedTime();
// cache wallet info last
synchronized (walletLock) {

View file

@ -257,8 +257,8 @@ public abstract class TradeStepView extends AnchorPane {
}
});
if (trade.wasWalletPolled.get()) addTradeStateSubscription();
else trade.wasWalletPolled.addListener((observable, oldValue, newValue) -> {
if (trade.wasWalletSyncedAndPolledProperty.get()) addTradeStateSubscription();
else trade.wasWalletSyncedAndPolledProperty.addListener((observable, oldValue, newValue) -> {
if (newValue) addTradeStateSubscription();
});