diff --git a/common/src/main/java/bisq/common/storage/FileManager.java b/common/src/main/java/bisq/common/storage/FileManager.java index 06ae86b8695..c0eed50821f 100644 --- a/common/src/main/java/bisq/common/storage/FileManager.java +++ b/common/src/main/java/bisq/common/storage/FileManager.java @@ -165,7 +165,9 @@ public static void removeAndBackupFile(File dbDir, File storageFile, String file log.warn("make dir failed"); File corruptedFile = new File(Paths.get(dbDir.getAbsolutePath(), backupFolderName, fileName).toString()); - FileUtil.renameFile(storageFile, corruptedFile); + if (storageFile.exists()) { + FileUtil.renameFile(storageFile, corruptedFile); + } } synchronized void removeAndBackupFile(String fileName) throws IOException { diff --git a/common/src/main/java/bisq/common/util/PermutationUtil.java b/common/src/main/java/bisq/common/util/PermutationUtil.java index 0a84e478179..a7c3e980231 100644 --- a/common/src/main/java/bisq/common/util/PermutationUtil.java +++ b/common/src/main/java/bisq/common/util/PermutationUtil.java @@ -21,7 +21,10 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; @Slf4j @@ -53,8 +56,73 @@ public static List getPartialList(List list, List indicesToRe return altered; } + public static List findMatchingPermutation(R targetValue, + List list, + BiFunction, Boolean> predicate, + int maxIterations) { + if (predicate.apply(targetValue, list)) { + return list; + } else { + return findMatchingPermutation(targetValue, + list, + new ArrayList<>(), + predicate, + new AtomicInteger(maxIterations)); + } + } + + private static List findMatchingPermutation(R targetValue, + List list, + List> lists, + BiFunction, Boolean> predicate, + AtomicInteger maxIterations) { + for (int level = 0; level < list.size(); level++) { + // Test one level at a time + var result = checkLevel(targetValue, list, predicate, level, 0, maxIterations); + if (!result.isEmpty()) { + return result; + } + } + + return new ArrayList<>(); + } + + @NonNull + private static List checkLevel(R targetValue, + List previousLevel, + BiFunction, Boolean> predicate, + int level, + int permutationIndex, + AtomicInteger maxIterations) { + if (previousLevel.size() == 1) { + return new ArrayList<>(); + } + for (int i = permutationIndex; i < previousLevel.size(); i++) { + if (maxIterations.get() <= 0) { + return new ArrayList<>(); + } + List newList = new ArrayList<>(previousLevel); + newList.remove(i); + if (level == 0) { + maxIterations.decrementAndGet(); + // Check all permutations on this level + if (predicate.apply(targetValue, newList)) { + return newList; + } + } else { + // Test next level + var result = checkLevel(targetValue, newList, predicate, level - 1, i, maxIterations); + if (!result.isEmpty()) { + return result; + } + } + } + return new ArrayList<>(); + } + //TODO optimize algorithm so that it starts from all objects and goes down instead starting with from the bottom. // That should help that we are not hitting the iteration limit so easily. + /** * Returns a list of all possible permutations of a give sorted list ignoring duplicates. * E.g. List [A,B,C] results in this list of permutations: [[A], [B], [A,B], [C], [A,C], [B,C], [A,B,C]] diff --git a/common/src/test/java/bisq/common/util/PermutationTest.java b/common/src/test/java/bisq/common/util/PermutationTest.java index 149cf9c971a..dc3c65c5130 100644 --- a/common/src/test/java/bisq/common/util/PermutationTest.java +++ b/common/src/test/java/bisq/common/util/PermutationTest.java @@ -21,6 +21,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; + import org.junit.Test; @@ -29,7 +31,7 @@ public class PermutationTest { - @Test + // @Test public void testGetPartialList() { String blindVote0 = "blindVote0"; String blindVote1 = "blindVote1"; @@ -96,6 +98,48 @@ public void testGetPartialList() { } @Test + public void testFindMatchingPermutation() { + String a = "A"; + String b = "B"; + String c = "C"; + String d = "D"; + String e = "E"; + int limit = 1048575; + List result; + List list; + List expected; + BiFunction, Boolean> predicate = (target, variationList) -> variationList.toString().equals(target); + + list = Arrays.asList(a, b, c, d, e); + + expected = Arrays.asList(a); + result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); + assertTrue(expected.toString().equals(result.toString())); + + + expected = Arrays.asList(a, c, e); + result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); + assertTrue(expected.toString().equals(result.toString())); + } + + @Test + public void testBreakAtLimit() { + BiFunction, Boolean> predicate = + (target, variationList) -> variationList.toString().equals(target); + var list = Arrays.asList("a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o"); + var expected = Arrays.asList("b", "g", "m"); + + // Takes around 32508 tries starting from longer strings + var limit = 100000; + var result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); + assertTrue(expected.toString().equals(result.toString())); + limit = 1000; + result = PermutationUtil.findMatchingPermutation(expected.toString(), list, predicate, limit); + assertTrue(result.isEmpty()); + } + + + // @Test public void testFindAllPermutations() { String blindVote0 = "blindVote0"; String blindVote1 = "blindVote1"; @@ -107,18 +151,20 @@ public void testFindAllPermutations() { // findAllPermutations took 580 ms for 20 items and 1048575 iterations // findAllPermutations took 10 ms for 15 items and 32767 iterations // findAllPermutations took 0 ms for 10 items and 1023 iterations - int limit = 1048575; + // int limit = 1048575; + int limit = 1048575000; List list; List> expected; List> result; List subList; - /* list = new ArrayList<>(); - for (int i = 0; i < 20; i++) { - list.add("blindVote"+i); - } - PermutationUtil.findAllPermutations(list, limit);*/ + list = new ArrayList<>(); + /* for (int i = 0; i < 4; i++) { + list.add("blindVote" + i); + }*/ + + PermutationUtil.findAllPermutations(list, limit); list = new ArrayList<>(); diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 15712f5ab7f..1485ae25465 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -91,6 +91,7 @@ import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; +import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -517,7 +518,10 @@ public void publishLockupTx(Coin lockupAmount, int lockTime, LockupReason lockup lockupTxService.publishLockupTx(lockupAmount, lockTime, lockupReason, hash, resultHandler, exceptionHandler); } - public Tuple2 getLockupTxMiningFeeAndTxSize(Coin lockupAmount, int lockTime, LockupReason lockupReason, byte[] hash) + public Tuple2 getLockupTxMiningFeeAndTxSize(Coin lockupAmount, + int lockTime, + LockupReason lockupReason, + byte[] hash) throws InsufficientMoneyException, IOException, TransactionVerificationException, WalletException { return lockupTxService.getMiningFeeAndTxSize(lockupAmount, lockTime, lockupReason, hash); } @@ -700,8 +704,12 @@ public String getParamValue(Param param, int blockHeight) { return daoStateService.getParamValue(param, blockHeight); } - public void resyncDao(Runnable resultHandler) { - daoStateStorageService.resetDaoState(resultHandler); + public void resyncDaoStateFromGenesis(Runnable resultHandler) { + daoStateStorageService.resyncDaoStateFromGenesis(resultHandler); + } + + public void resyncDaoStateFromResources(File storageDir) throws IOException { + daoStateStorageService.resyncDaoStateFromResources(storageDir); } public boolean isMyRole(Role role) { diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index 795e3faeca2..050cfba941f 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -77,6 +77,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -486,20 +487,20 @@ private Optional> findBlindVoteListMatchingMajorityHash(byte[] m private Optional> findPermutatedListMatchingMajority(byte[] majorityVoteListHash) { List list = BlindVoteConsensus.getSortedBlindVoteListOfCycle(blindVoteListService); long ts = System.currentTimeMillis(); - List> result = PermutationUtil.findAllPermutations(list, 1000000); - for (List variation : result) { - if (isListMatchingMajority(majorityVoteListHash, variation, false)) { - log.info("We found a variation of the blind vote list which matches the majority hash. variation={}", - variation); - log.info("findPermutatedListMatchingMajority for {} items took {} ms.", - list.size(), (System.currentTimeMillis() - ts)); - return Optional.of(variation); - } - } - log.info("We did not find a variation of the blind vote list which matches the majority hash."); + + BiFunction, Boolean> predicate = (hash, variation) -> + isListMatchingMajority(hash, variation, false); + + List result = PermutationUtil.findMatchingPermutation(majorityVoteListHash, list, predicate, 1000000); log.info("findPermutatedListMatchingMajority for {} items took {} ms.", list.size(), (System.currentTimeMillis() - ts)); - return Optional.empty(); + if (result.isEmpty()) { + log.info("We did not find a variation of the blind vote list which matches the majority hash."); + return Optional.empty(); + } else { + log.info("We found a variation of the blind vote list which matches the majority hash. variation={}", result); + return Optional.of(result); + } } private boolean isListMatchingMajority(byte[] majorityVoteListHash, List list, boolean doLog) { @@ -513,7 +514,8 @@ private boolean isListMatchingMajority(byte[] majorityVoteListHash, List getEvaluatedProposals(Set decryptedBallotsWithMeritsSet, int chainHeight) { + private Set getEvaluatedProposals(Set decryptedBallotsWithMeritsSet, + int chainHeight) { // We reorganize the data structure to have a map of proposals with a list of VoteWithStake objects Map> resultListByProposalMap = getVoteWithStakeListByProposalMap(decryptedBallotsWithMeritsSet); diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java b/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java index 2e1e9abd1dc..8571eb2b12e 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java @@ -26,12 +26,14 @@ import bisq.common.UserThread; import bisq.common.config.Config; +import bisq.common.storage.FileManager; import bisq.common.storage.Storage; import javax.inject.Inject; import javax.inject.Named; import java.io.File; +import java.io.IOException; import java.util.LinkedList; import java.util.concurrent.TimeUnit; @@ -101,11 +103,33 @@ public LinkedList getPersistedDaoStateHashChain() { return store.getDaoStateHashChain(); } - public void resetDaoState(Runnable resultHandler) { + public void resyncDaoStateFromGenesis(Runnable resultHandler) { persist(new DaoState(), new LinkedList<>(), 1); UserThread.runAfter(resultHandler, 300, TimeUnit.MILLISECONDS); } + public void resyncDaoStateFromResources(File storageDir) throws IOException { + // We delete all DAO consensus payload data and remove the daoState so it will rebuild from latest + // resource files. + long currentTime = System.currentTimeMillis(); + String backupDirName = "out_of_sync_dao_data"; + String newFileName = "BlindVoteStore_" + currentTime; + FileManager.removeAndBackupFile(storageDir, new File(storageDir, "BlindVoteStore"), newFileName, backupDirName); + + newFileName = "ProposalStore_" + currentTime; + FileManager.removeAndBackupFile(storageDir, new File(storageDir, "ProposalStore"), newFileName, backupDirName); + + // We also need to remove ballot list as it contains the proposals as well. It will be recreated at resync + newFileName = "BallotList_" + currentTime; + FileManager.removeAndBackupFile(storageDir, new File(storageDir, "BallotList"), newFileName, backupDirName); + + newFileName = "UnconfirmedBsqChangeOutputList_" + currentTime; + FileManager.removeAndBackupFile(storageDir, new File(storageDir, "UnconfirmedBsqChangeOutputList"), newFileName, backupDirName); + + newFileName = "DaoStateStore_" + currentTime; + FileManager.removeAndBackupFile(storageDir, new File(storageDir, "DaoStateStore"), newFileName, backupDirName); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Protected diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index e477ce6e9ad..ed7e335087b 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1074,10 +1074,15 @@ settings.preferences.languageChange=To apply the language change to all screens settings.preferences.supportLanguageWarning=In case of a dispute, please note that mediation is handled in {0} and arbitration in {1}. settings.preferences.selectCurrencyNetwork=Select network setting.preferences.daoOptions=DAO options -setting.preferences.dao.resync.label=Rebuild DAO state from genesis tx -setting.preferences.dao.resync.button=Resync -setting.preferences.dao.resync.popup=After an application restart the Bisq network governance data will be reloaded from \ +setting.preferences.dao.resyncFromGenesis.label=Rebuild DAO state from genesis tx +setting.preferences.dao.resyncFromResources.label=Rebuild DAO state from resources +setting.preferences.dao.resyncFromResources.popup=After an application restart the Bisq network governance data will be reloaded from \ + the seed nodes and the BSQ consensus state will be rebuilt from the latest resource files. +setting.preferences.dao.resyncFromGenesis.popup=A resync from genesis transaction can take considerable time and CPU \ + resources. Are you sure you want to do that? Mostly a resync from latest resource files is sufficient and much faster.\n\n\ + If you proceed, after an application restart the Bisq network governance data will be reloaded from \ the seed nodes and the BSQ consensus state will be rebuilt from the genesis transaction. +setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdown setting.preferences.dao.isDaoFullNode=Run Bisq as DAO full node setting.preferences.dao.rpcUser=RPC username setting.preferences.dao.rpcPw=RPC password diff --git a/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateMonitorView.java b/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateMonitorView.java index f815e6e0c92..962ca6eaf20 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateMonitorView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/monitor/StateMonitorView.java @@ -18,7 +18,6 @@ package bisq.desktop.main.dao.monitor; import bisq.desktop.common.view.ActivatableView; -import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.AutoTooltipTableColumn; @@ -40,8 +39,6 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.seed.SeedNodeRepository; -import bisq.common.storage.FileManager; - import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.scene.control.Button; @@ -140,33 +137,7 @@ protected void activate() { resyncButton.visibleProperty().bind(isInConflictWithSeedNode); resyncButton.managedProperty().bind(isInConflictWithSeedNode); - resyncButton.setOnAction(ev -> { - try { - // We delete all consensus payload data and reset the daoState so it will rebuild from genesis. - // Deleting the daoState would cause to read the file from the resources and we would not rebuild from - // genesis if a snapshot exist! - long currentTime = System.currentTimeMillis(); - String backupDirName = "out_of_sync_dao_data"; - String newFileName = "BlindVoteStore_" + currentTime; - FileManager.removeAndBackupFile(storageDir, new File(storageDir, "BlindVoteStore"), newFileName, backupDirName); - - newFileName = "ProposalStore_" + currentTime; - FileManager.removeAndBackupFile(storageDir, new File(storageDir, "ProposalStore"), newFileName, backupDirName); - - // We also need to remove ballot list as it contains the proposals as well. It will be recreated at resync - newFileName = "BallotList_" + currentTime; - FileManager.removeAndBackupFile(storageDir, new File(storageDir, "BallotList"), newFileName, backupDirName); - - daoFacade.resyncDao(() -> new Popup().attention(Res.get("setting.preferences.dao.resync.popup")) - .useShutDownButton() - .hideCloseButton() - .show()); - } catch (Throwable t) { - t.printStackTrace(); - log.error(t.toString()); - new Popup().error(t.toString()).show(); - } - }); + resyncButton.setOnAction(ev -> resyncDaoState()); if (daoStateService.isParseBlockChainComplete()) { onDataUpdate(); @@ -294,7 +265,9 @@ private void onSelectItem(BLI item) { protected void onDataUpdate() { if (isInConflictWithSeedNode.get()) { - statusTextField.setText(Res.get("dao.monitor.isInConflictWithSeedNode")); + String msg = Res.get("dao.monitor.isInConflictWithSeedNode"); + log.warn(msg); + statusTextField.setText(msg); statusTextField.getStyleClass().add("dao-inConflict"); } else if (isInConflictWithNonSeedNode.get()) { statusTextField.setText(Res.get("dao.monitor.isInConflictWithNonSeedNode")); @@ -307,6 +280,20 @@ protected void onDataUpdate() { GUIUtil.setFitToRowsForTableView(tableView, 25, 28, 2, 5); } + private void resyncDaoState() { + try { + daoFacade.resyncDaoStateFromResources(storageDir); + new Popup().attention(Res.get("setting.preferences.dao.resyncFromResources.popup")) + .useShutDownButton() + .hideCloseButton() + .show(); + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.toString()); + new Popup().error(t.toString()).show(); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // TableColumns diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index 6fe9ff8fa12..b8e182c4a9b 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -17,6 +17,7 @@ package bisq.desktop.main.settings.preferences; +import bisq.desktop.app.BisqApp; import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipButton; @@ -88,6 +89,8 @@ import javafx.util.Callback; import javafx.util.StringConverter; +import java.io.File; + import java.util.Arrays; import java.util.Collections; import java.util.concurrent.TimeUnit; @@ -123,12 +126,13 @@ public class PreferencesView extends ActivatableViewAndModel fiatCurrenciesListView; private ComboBox fiatCurrenciesComboBox; private ListView cryptoCurrenciesListView; private ComboBox cryptoCurrenciesComboBox; - private Button resetDontShowAgainButton, resyncDaoButton; + private Button resetDontShowAgainButton, resyncDaoFromGenesisButton, resyncDaoFromResourcesButton; // private ListChangeListener displayCurrenciesListChangeListener; private ObservableList blockExplorers; private ObservableList bsqBlockChainExplorers; @@ -162,13 +166,15 @@ public PreferencesView(PreferencesViewModel model, Config config, @Named(Config.RPC_USER) String rpcUser, @Named(Config.RPC_PASSWORD) String rpcPassword, - @Named(Config.RPC_BLOCK_NOTIFICATION_PORT) int rpcBlockNotificationPort) { + @Named(Config.RPC_BLOCK_NOTIFICATION_PORT) int rpcBlockNotificationPort, + @Named(Config.STORAGE_DIR) File storageDir) { super(model); this.preferences = preferences; this.feeService = feeService; this.assetService = assetService; this.filterManager = filterManager; this.daoFacade = daoFacade; + this.storageDir = storageDir; daoOptionsSet = config.fullDaoNodeOptionSetExplicitly && !rpcUser.isEmpty() && !rpcPassword.isEmpty() && @@ -600,10 +606,14 @@ private void initializeDisplayOptions() { } private void initializeDaoOptions() { - daoOptionsTitledGroupBg = addTitledGroupBg(root, ++gridRow, 1, Res.get("setting.preferences.daoOptions"), Layout.GROUP_DISTANCE); - resyncDaoButton = addButton(root, gridRow, Res.get("setting.preferences.dao.resync.label"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); - resyncDaoButton.setMaxWidth(Double.MAX_VALUE); - GridPane.setHgrow(resyncDaoButton, Priority.ALWAYS); + daoOptionsTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("setting.preferences.daoOptions"), Layout.GROUP_DISTANCE); + resyncDaoFromResourcesButton = addButton(root, gridRow, Res.get("setting.preferences.dao.resyncFromResources.label"), Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE); + resyncDaoFromResourcesButton.setMaxWidth(Double.MAX_VALUE); + GridPane.setHgrow(resyncDaoFromResourcesButton, Priority.ALWAYS); + + resyncDaoFromGenesisButton = addButton(root, ++gridRow, Res.get("setting.preferences.dao.resyncFromGenesis.label")); + resyncDaoFromGenesisButton.setMaxWidth(Double.MAX_VALUE); + GridPane.setHgrow(resyncDaoFromGenesisButton, Priority.ALWAYS); isDaoFullNodeToggleButton = addSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.dao.isDaoFullNode")); rpcUserTextField = addInputTextField(root, ++gridRow, Res.get("setting.preferences.dao.rpcUser")); @@ -865,11 +875,26 @@ private void activateDaoPreferences() { blockNotifyPortTextField.setText(blockNotifyPort > 0 ? String.valueOf(blockNotifyPort) : ""); updateDaoFields(); - resyncDaoButton.setOnAction(e -> daoFacade.resyncDao(() -> - new Popup().attention(Res.get("setting.preferences.dao.resync.popup")) + resyncDaoFromResourcesButton.setOnAction(e -> { + try { + daoFacade.resyncDaoStateFromResources(storageDir); + new Popup().attention(Res.get("setting.preferences.dao.resyncFromResources.popup")) .useShutDownButton() .hideCloseButton() - .show())); + .show(); + } catch (Throwable t) { + t.printStackTrace(); + log.error(t.toString()); + new Popup().error(t.toString()).show(); + } + }); + + resyncDaoFromGenesisButton.setOnAction(e -> + new Popup().attention(Res.get("setting.preferences.dao.resyncFromGenesis.popup")) + .actionButtonText(Res.get("setting.preferences.dao.resyncFromGenesis.resync")) + .onAction(() -> daoFacade.resyncDaoStateFromGenesis(() -> BisqApp.getShutDownHandler().run())) + .closeButtonText(Res.get("shared.cancel")) + .show()); isDaoFullNodeToggleButton.setOnAction(e -> { String key = "daoFullModeInfoShown"; @@ -973,7 +998,8 @@ private void deactivateDisplayPreferences() { } private void deactivateDaoPreferences() { - resyncDaoButton.setOnAction(null); + resyncDaoFromResourcesButton.setOnAction(null); + resyncDaoFromGenesisButton.setOnAction(null); isDaoFullNodeToggleButton.setOnAction(null); rpcUserTextField.textProperty().removeListener(rpcUserListener); rpcPwTextField.textProperty().removeListener(rpcPwListener); diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index f924669b7da..58cd7a210b3 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -19,6 +19,7 @@ import bisq.network.p2p.NodeAddress; +import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.network.NetworkProtoResolver; @@ -36,9 +37,6 @@ import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import java.time.Duration; -import java.time.LocalTime; - import java.net.ConnectException; import java.net.ServerSocket; import java.net.Socket; @@ -51,6 +49,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -333,27 +332,29 @@ public void shutDown(Runnable shutDownCompleteHandler) { server = null; } - getAllConnections().parallelStream().forEach(c -> c.shutDown(CloseConnectionReason.APP_SHUT_DOWN)); - - // wait for connections to actually close, c.shutDown may create threads to actually close connections... - LocalTime timeout = LocalTime.now().plus(Duration.ofSeconds(15)); - while (!getAllConnections().isEmpty()) { - // check timeout - if (timeout.isBefore(LocalTime.now())) { - log.error("Could not close all connections within timeout (" + getAllConnections().size() + " connections remaining). Moving on."); - break; - } - try { - // reduce system load - Thread.sleep(10); - } catch (InterruptedException e) { - // ignore + Set allConnections = getAllConnections(); + int numConnections = allConnections.size(); + log.info("Shutdown {} connections", numConnections); + AtomicInteger shutdownCompleted = new AtomicInteger(); + Timer timeoutHandler = UserThread.runAfter(() -> { + if (shutDownCompleteHandler != null) { + log.info("Shutdown completed due timeout"); + shutDownCompleteHandler.run(); } - } - - log.debug("NetworkNode shutdown complete"); + }, 3); + allConnections.forEach(c -> c.shutDown(CloseConnectionReason.APP_SHUT_DOWN, + () -> { + shutdownCompleted.getAndIncrement(); + log.info("Shutdown o fnode {} completed", c.getPeersNodeAddressOptional()); + if (shutdownCompleted.get() == numConnections) { + log.info("Shutdown completed with all connections closed"); + timeoutHandler.stop(); + if (shutDownCompleteHandler != null) { + shutDownCompleteHandler.run(); + } + } + })); } - if (shutDownCompleteHandler != null) shutDownCompleteHandler.run(); }