fix app hanging after connection change with parallel handling

This commit is contained in:
woodser 2023-11-18 14:36:42 -05:00
parent 5e00612f66
commit 5fc67ec65a
3 changed files with 107 additions and 97 deletions

View File

@ -53,6 +53,7 @@ public final class CoreMoneroConnectionsService {
private final Object lock = new Object(); private final Object lock = new Object();
private final Object listenersLock = new Object();
private final Config config; private final Config config;
private final CoreContext coreContext; private final CoreContext coreContext;
private final Preferences preferences; private final Preferences preferences;
@ -146,7 +147,7 @@ public final class CoreMoneroConnectionsService {
} }
public void addConnectionListener(MoneroConnectionManagerListener listener) { public void addConnectionListener(MoneroConnectionManagerListener listener) {
synchronized (lock) { synchronized (listenersLock) {
listeners.add(listener); listeners.add(listener);
} }
} }
@ -512,9 +513,11 @@ public final class CoreMoneroConnectionsService {
} }
updatePolling(); updatePolling();
// notify listeners // notify listeners in parallel
synchronized (lock) { synchronized (listenersLock) {
for (MoneroConnectionManagerListener listener : listeners) listener.onConnectionChanged(currentConnection); for (MoneroConnectionManagerListener listener : listeners) {
new Thread(() -> listener.onConnectionChanged(currentConnection)).start();
}
} }
} }
@ -537,64 +540,68 @@ public final class CoreMoneroConnectionsService {
private void stopPolling() { private void stopPolling() {
synchronized (lock) { synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop(); if (daemonPollLooper != null) {
daemonPollLooper.stop();
daemonPollLooper = null;
}
} }
} }
private void pollDaemonInfo() { private void pollDaemonInfo() {
if (isShutDownStarted) return; synchronized (lock) {
try { if (isShutDownStarted) return;
log.debug("Polling daemon info"); try {
if (daemon == null) throw new RuntimeException("No daemon connection"); log.debug("Polling daemon info");
synchronized (this) { if (daemon == null) throw new RuntimeException("No daemon connection");
lastInfo = daemon.getInfo(); lastInfo = daemon.getInfo();
} chainHeight.set(lastInfo.getTargetHeight() == 0 ? lastInfo.getHeight() : lastInfo.getTargetHeight());
chainHeight.set(lastInfo.getTargetHeight() == 0 ? lastInfo.getHeight() : lastInfo.getTargetHeight());
// set peer connections // set peer connections
// TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections // TODO: peers often uknown due to restricted RPC call, skipping call to get peer connections
// try { // try {
// peers.set(getOnlinePeers()); // peers.set(getOnlinePeers());
// } catch (Exception err) { // } catch (Exception err) {
// // TODO: peers unknown due to restricted RPC call // // TODO: peers unknown due to restricted RPC call
// } // }
// numPeers.set(peers.get().size()); // numPeers.set(peers.get().size());
numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections()); numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections());
peers.set(new ArrayList<MoneroPeer>()); peers.set(new ArrayList<MoneroPeer>());
// handle error recovery // handle error recovery
if (lastErrorTimestamp != null) { if (lastErrorTimestamp != null) {
log.info("Successfully fetched daemon info after previous error"); log.info("Successfully fetched daemon info after previous error");
lastErrorTimestamp = null; lastErrorTimestamp = null;
} }
// update and notify connected state // update and notify connected state
if (!Boolean.TRUE.equals(connectionManager.isConnected())) { if (!Boolean.TRUE.equals(connectionManager.isConnected())) {
connectionManager.checkConnection(); connectionManager.checkConnection();
} }
// clear error message // clear error message
if (Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) { if (Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) {
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null); HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null);
} }
} catch (Exception e) { } catch (Exception e) {
// log error message periodically // skip if shut down or connected
if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) { if (isShutDownStarted || Boolean.TRUE.equals(isConnected())) return;
lastErrorTimestamp = System.currentTimeMillis();
log.warn("Could not update daemon info: " + e.getMessage()); // log error message periodically
if (DevEnv.isDevMode()) e.printStackTrace(); if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) {
} lastErrorTimestamp = System.currentTimeMillis();
log.warn("Could not update daemon info: " + e.getMessage());
if (DevEnv.isDevMode()) e.printStackTrace();
}
// check connection which notifies of changes // check connection which notifies of changes
synchronized (this) {
if (connectionManager.getAutoSwitch()) connectionManager.setConnection(connectionManager.getBestAvailableConnection()); if (connectionManager.getAutoSwitch()) connectionManager.setConnection(connectionManager.getBestAvailableConnection());
else connectionManager.checkConnection(); else connectionManager.checkConnection();
}
// set error message // set error message
if (!Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) { if (!Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) {
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage()); HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage());
}
} }
} }
} }

View File

@ -587,8 +587,10 @@ public abstract class Trade implements Tradable, Model {
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
}); });
// listen to daemon connection // handle daemon changes with max parallelization
xmrWalletService.getConnectionsService().addConnectionListener(newConnection -> onConnectionChanged(newConnection)); xmrWalletService.getConnectionsService().addConnectionListener(newConnection -> {
HavenoUtils.submitTask(() -> onConnectionChanged(newConnection));
});
// check if done // check if done
if (isPayoutUnlocked()) { if (isPayoutUnlocked()) {

View File

@ -3,7 +3,6 @@ package haveno.core.xmr.wallet;
import com.google.common.util.concurrent.Service.State; import com.google.common.util.concurrent.Service.State;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import common.utils.GenUtils;
import common.utils.JsonUtils; import common.utils.JsonUtils;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.common.config.Config; import haveno.common.config.Config;
@ -97,6 +96,7 @@ public class XmrWalletService {
private static final String ADDRESS_FILE_POSTFIX = ".address.txt"; private static final String ADDRESS_FILE_POSTFIX = ".address.txt";
private static final int NUM_MAX_BACKUP_WALLETS = 1; private static final int NUM_MAX_BACKUP_WALLETS = 1;
private static final int MONERO_LOG_LEVEL = 0; private static final int MONERO_LOG_LEVEL = 0;
private static final int MAX_SYNC_ATTEMPTS = 3;
private static final boolean PRINT_STACK_TRACE = false; private static final boolean PRINT_STACK_TRACE = false;
private final Preferences preferences; private final Preferences preferences;
@ -651,14 +651,20 @@ public class XmrWalletService {
private void initialize() { private void initialize() {
// set and listen to daemon connection
connectionsService.addConnectionListener(newConnection -> {
onConnectionChanged(newConnection);
});
// initialize main wallet if connected or previously created // initialize main wallet if connected or previously created
maybeInitMainWallet(true); maybeInitMainWallet(true);
// set and listen to daemon connection
connectionsService.addConnectionListener(newConnection -> onConnectionChanged(newConnection));
} }
private void maybeInitMainWallet(boolean sync) { private void maybeInitMainWallet(boolean sync) {
maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS);
}
private void maybeInitMainWallet(boolean sync, int numAttempts) {
synchronized (walletLock) { synchronized (walletLock) {
// open or create wallet main wallet // open or create wallet main wallet
@ -676,52 +682,47 @@ public class XmrWalletService {
// sync wallet and register listener // sync wallet and register listener
if (wallet != null) { if (wallet != null) {
log.info("Monero wallet uri={}, path={}", wallet.getRpcConnection().getUri(), wallet.getPath()); log.info("Monero wallet uri={}, path={}", wallet.getRpcConnection().getUri(), wallet.getPath());
if (sync) { if (sync && numAttempts > 0) {
int maxAttempts = 3; try {
for (int i = 0; i < maxAttempts; i++) {
try { // sync main wallet
log.info("Syncing main wallet");
// sync main wallet long time = System.currentTimeMillis();
log.info("Syncing main wallet"); wallet.sync(); // blocking
long time = System.currentTimeMillis(); wasWalletSynced = true;
wallet.sync(); // blocking log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
wasWalletSynced = true; wallet.startSyncing(connectionsService.getRefreshPeriodMs());
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) log.info("Monero wallet balance={}, unlocked balance={}", wallet.getBalance(0), wallet.getUnlockedBalance(0));
wallet.startSyncing(connectionsService.getRefreshPeriodMs());
if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) log.info("Monero wallet balance={}, unlocked balance={}", wallet.getBalance(0), wallet.getUnlockedBalance(0)); // reapply connection after wallet synced
onConnectionChanged(connectionsService.getConnection());
// reapply connection after wallet synced
onConnectionChanged(connectionsService.getConnection());
// TODO: using this to signify both daemon and wallet synced, use separate sync handlers? // TODO: using this to signify both daemon and wallet synced, use separate sync handlers?
connectionsService.doneDownload(); connectionsService.doneDownload();
// notify setup that main wallet is initialized // notify setup that main wallet is initialized
// TODO: app fully initializes after this is set to true, even though wallet might not be initialized if unconnected. wallet will be created when connection detected // TODO: app fully initializes after this is set to true, even though wallet might not be initialized if unconnected. wallet will be created when connection detected
// refactor startup to call this and sync off main thread? but the calls to e.g. getBalance() fail with 'wallet and network is not yet initialized' // refactor startup to call this and sync off main thread? but the calls to e.g. getBalance() fail with 'wallet and network is not yet initialized'
HavenoUtils.havenoSetup.getWalletInitialized().set(true);
// save but skip backup on initialization
saveMainWallet(false);
} catch (Exception e) {
log.warn("Error syncing main wallet: {}", e.getMessage());
if (numAttempts <= 1) {
log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts);
HavenoUtils.havenoSetup.getWalletInitialized().set(true); HavenoUtils.havenoSetup.getWalletInitialized().set(true);
// save but skip backup on initialization
saveMainWallet(false); saveMainWallet(false);
break;
} catch (Exception e) {
log.warn("Error syncing main wallet: {}", e.getMessage());
if (i == maxAttempts - 1) {
log.warn("Failed to sync main wallet after {} attempts. Opening app without syncing", maxAttempts);
HavenoUtils.havenoSetup.getWalletInitialized().set(true);
saveMainWallet(false);
// reschedule to init main wallet // reschedule to init main wallet
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
new Thread(() -> { new Thread(() -> maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS)).start();
log.warn("Restarting attempts to sync main wallet"); }, connectionsService.getRefreshPeriodMs() / 1000);
maybeInitMainWallet(true); } else {
}); log.warn("Trying again in {} seconds", connectionsService.getRefreshPeriodMs() / 1000);
}, connectionsService.getRefreshPeriodMs() / 1000); UserThread.runAfter(() -> {
} else { new Thread(() -> maybeInitMainWallet(true, numAttempts - 1)).start();
log.warn("Trying again in {} seconds", connectionsService.getRefreshPeriodMs() / 1000); }, connectionsService.getRefreshPeriodMs() / 1000);
GenUtils.waitFor(connectionsService.getRefreshPeriodMs());
}
} }
} }
} }