mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-05-19 23:10:48 -04:00
add local storage for seed map
This commit is contained in:
parent
9ef8b42509
commit
0d9e0d0f31
9 changed files with 34 additions and 23 deletions
|
@ -17,6 +17,6 @@
|
|||
|
||||
package io.bitsquare.common.handlers;
|
||||
|
||||
public interface ResultHandler extends Runnable {
|
||||
public interface ResultHandler {
|
||||
void handleResult();
|
||||
}
|
||||
|
|
318
common/src/main/java/io/bitsquare/storage/FileManager.java
Normal file
318
common/src/main/java/io/bitsquare/storage/FileManager.java
Normal file
|
@ -0,0 +1,318 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2013 Google Inc.
|
||||
* Copyright 2014 Andreas Schildbach
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.bitsquare.storage;
|
||||
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import io.bitsquare.common.UserThread;
|
||||
import org.bitcoinj.core.Utils;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* Borrowed from BitcoinJ WalletFiles
|
||||
* A class that handles atomic and optionally delayed writing of a file to disk.
|
||||
* It can be useful to delay writing of a file to disk on slow devices.
|
||||
* By coalescing writes and doing serialization
|
||||
* and disk IO on a background thread performance can be improved.
|
||||
*/
|
||||
public class FileManager<T> {
|
||||
private static final Logger log = LoggerFactory.getLogger(FileManager.class);
|
||||
private static final ReentrantLock lock = Threading.lock("FileManager");
|
||||
private static Thread.UncaughtExceptionHandler uncaughtExceptionHandler;
|
||||
|
||||
private final File dir;
|
||||
private final File storageFile;
|
||||
private final ScheduledThreadPoolExecutor executor;
|
||||
private final AtomicBoolean savePending;
|
||||
private final long delay;
|
||||
private final TimeUnit delayTimeUnit;
|
||||
private final Callable<Void> saver;
|
||||
private T serializable;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public FileManager(File dir, File storageFile, long delay, TimeUnit delayTimeUnit) {
|
||||
this.dir = dir;
|
||||
this.storageFile = storageFile;
|
||||
|
||||
ThreadFactoryBuilder builder = new ThreadFactoryBuilder()
|
||||
.setDaemon(true)
|
||||
.setNameFormat("FileManager thread")
|
||||
.setPriority(Thread.MIN_PRIORITY); // Avoid competing with the GUI thread.
|
||||
|
||||
// An executor that starts up threads when needed and shuts them down later.
|
||||
executor = new ScheduledThreadPoolExecutor(1, builder.build());
|
||||
executor.setKeepAliveTime(5, TimeUnit.SECONDS);
|
||||
executor.allowCoreThreadTimeOut(true);
|
||||
executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
|
||||
|
||||
// File must only be accessed from the auto-save executor from now on, to avoid simultaneous access.
|
||||
savePending = new AtomicBoolean();
|
||||
this.delay = delay;
|
||||
this.delayTimeUnit = checkNotNull(delayTimeUnit);
|
||||
|
||||
saver = () -> {
|
||||
// Runs in an auto save thread.
|
||||
if (!savePending.getAndSet(false)) {
|
||||
// Some other scheduled request already beat us to it.
|
||||
return null;
|
||||
}
|
||||
saveNowInternal(serializable);
|
||||
return null;
|
||||
};
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
FileManager.this.shutDown();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Actually write the wallet file to disk, using an atomic rename when possible. Runs on the current thread.
|
||||
*/
|
||||
public void saveNow(T serializable) {
|
||||
saveNowInternal(serializable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues up a save in the background. Useful for not very important wallet changes.
|
||||
*/
|
||||
public void saveLater(T serializable) {
|
||||
this.serializable = serializable;
|
||||
|
||||
if (savePending.getAndSet(true))
|
||||
return; // Already pending.
|
||||
executor.schedule(saver, delay, delayTimeUnit);
|
||||
}
|
||||
|
||||
public T read(File file) {
|
||||
log.debug("read" + file);
|
||||
lock.lock();
|
||||
try (final FileInputStream fileInputStream = new FileInputStream(file);
|
||||
final ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
|
||||
return (T) objectInputStream.readObject();
|
||||
} catch (Throwable t) {
|
||||
log.error("Exception at read: " + t.getMessage());
|
||||
return null;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFile(String fileName) {
|
||||
log.debug("removeFile" + fileName);
|
||||
File file = new File(dir, fileName);
|
||||
lock.lock();
|
||||
try {
|
||||
boolean result = file.delete();
|
||||
if (!result)
|
||||
log.warn("Could not delete file: " + file.toString());
|
||||
|
||||
File backupDir = new File(Paths.get(dir.getAbsolutePath(), "backup").toString());
|
||||
if (backupDir.exists()) {
|
||||
File backupFile = new File(Paths.get(dir.getAbsolutePath(), "backup", fileName).toString());
|
||||
if (backupFile.exists()) {
|
||||
result = backupFile.delete();
|
||||
if (!result)
|
||||
log.warn("Could not delete backupFile: " + file.toString());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shut down auto-saving.
|
||||
*/
|
||||
public void shutDown() {
|
||||
/* if (serializable != null)
|
||||
log.debug("shutDown " + serializable.getClass().getSimpleName());
|
||||
else
|
||||
log.debug("shutDown");*/
|
||||
|
||||
executor.shutdown();
|
||||
try {
|
||||
//executor.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS); // forever
|
||||
executor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public void removeAndBackupFile(String fileName) throws IOException {
|
||||
lock.lock();
|
||||
try {
|
||||
File corruptedBackupDir = new File(Paths.get(dir.getAbsolutePath(), "corrupted").toString());
|
||||
if (!corruptedBackupDir.exists())
|
||||
if (!corruptedBackupDir.mkdir())
|
||||
log.warn("make dir failed");
|
||||
|
||||
File corruptedFile = new File(Paths.get(dir.getAbsolutePath(), "corrupted", fileName).toString());
|
||||
renameTempFileToFile(storageFile, corruptedFile);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void backupFile(String fileName) throws IOException {
|
||||
lock.lock();
|
||||
try {
|
||||
File backupDir = new File(Paths.get(dir.getAbsolutePath(), "backup").toString());
|
||||
if (!backupDir.exists())
|
||||
if (!backupDir.mkdir())
|
||||
log.warn("make dir failed");
|
||||
|
||||
File backupFile = new File(Paths.get(dir.getAbsolutePath(), "backup", fileName).toString());
|
||||
Files.copy(storageFile, backupFile);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void saveNowInternal(T serializable) {
|
||||
long now = System.currentTimeMillis();
|
||||
saveToFile(serializable, dir, storageFile);
|
||||
UserThread.execute(() -> log.info("Save {} completed in {}msec", storageFile, System.currentTimeMillis() - now));
|
||||
}
|
||||
|
||||
private void saveToFile(T serializable, File dir, File storageFile) {
|
||||
lock.lock();
|
||||
File tempFile = null;
|
||||
FileOutputStream fileOutputStream = null;
|
||||
ObjectOutputStream objectOutputStream = null;
|
||||
try {
|
||||
if (!dir.exists())
|
||||
if (!dir.mkdir())
|
||||
log.warn("make dir failed");
|
||||
|
||||
tempFile = File.createTempFile("temp", null, dir);
|
||||
|
||||
// Don't use auto closeable resources in try() as we would need too many try/catch clauses (for tempFile)
|
||||
// and we need to close it
|
||||
// manually before replacing file with temp file
|
||||
fileOutputStream = new FileOutputStream(tempFile);
|
||||
objectOutputStream = new ObjectOutputStream(fileOutputStream);
|
||||
|
||||
// TODO ConcurrentModificationException happens sometimes at that line
|
||||
objectOutputStream.writeObject(serializable);
|
||||
// Attempt to force the bits to hit the disk. In reality the OS or hard disk itself may still decide
|
||||
// to not write through to physical media for at least a few seconds, but this is the best we can do.
|
||||
fileOutputStream.flush();
|
||||
fileOutputStream.getFD().sync();
|
||||
|
||||
// Close resources before replacing file with temp file because otherwise it causes problems on windows
|
||||
// when rename temp file
|
||||
fileOutputStream.close();
|
||||
objectOutputStream.close();
|
||||
|
||||
renameTempFileToFile(tempFile, storageFile);
|
||||
} catch (Throwable t) {
|
||||
log.debug("storageFile " + storageFile.toString());
|
||||
t.printStackTrace();
|
||||
log.error("Error at saveToFile: " + t.getMessage());
|
||||
} finally {
|
||||
if (tempFile != null && tempFile.exists()) {
|
||||
log.warn("Temp file still exists after failed save. storageFile=" + storageFile);
|
||||
if (!tempFile.delete())
|
||||
log.error("Cannot delete temp file.");
|
||||
}
|
||||
|
||||
try {
|
||||
if (objectOutputStream != null)
|
||||
objectOutputStream.close();
|
||||
if (fileOutputStream != null)
|
||||
fileOutputStream.close();
|
||||
} catch (IOException e) {
|
||||
// We swallow that
|
||||
e.printStackTrace();
|
||||
log.error("Cannot close resources." + e.getMessage());
|
||||
}
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void renameTempFileToFile(File tempFile, File file) throws IOException {
|
||||
lock.lock();
|
||||
try {
|
||||
if (Utils.isWindows()) {
|
||||
// Work around an issue on Windows whereby you can't rename over existing files.
|
||||
final File canonical = file.getCanonicalFile();
|
||||
if (canonical.exists() && !canonical.delete()) {
|
||||
throw new IOException("Failed to delete canonical file for replacement with save");
|
||||
}
|
||||
if (!tempFile.renameTo(canonical)) {
|
||||
throw new IOException("Failed to rename " + tempFile + " to " + canonical);
|
||||
}
|
||||
} else if (!tempFile.renameTo(file)) {
|
||||
throw new IOException("Failed to rename " + tempFile + " to " + file);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
150
common/src/main/java/io/bitsquare/storage/Storage.java
Normal file
150
common/src/main/java/io/bitsquare/storage/Storage.java
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.storage;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* That class handles the storage of a particular object to disk using Java serialisation.
|
||||
* To support evolving versions of the serialised data we need to take care that we don't break the object structure.
|
||||
* Java serialisation is tolerant with added fields, but removing or changing existing fields will break the backwards compatibility.
|
||||
* Alternative frameworks for serialisation like Kyro or mapDB have shown problems with version migration, so we stuck with plain Java
|
||||
* serialisation.
|
||||
* <p>
|
||||
* For every data object we write a separate file to minimize the risk of corrupted files in case of inconsistency from newer versions.
|
||||
* In case of a corrupted file we backup the old file to a separate directory, so if it holds critical data it might be helpful for recovery.
|
||||
* <p>
|
||||
* We also backup at first read the file, so we have a valid file form the latest version in case a write operation corrupted the file.
|
||||
* <p>
|
||||
* The read operation is triggered just at object creation (startup) and is at the moment not executed on a background thread to avoid asynchronous behaviour.
|
||||
* As the data are small and it is just one read access the performance penalty is small and might be even worse to create and setup a thread for it.
|
||||
* <p>
|
||||
* The write operation used a background thread and supports a delayed write to avoid too many repeated write operations.
|
||||
*/
|
||||
public class Storage<T extends Serializable> {
|
||||
private static final Logger log = LoggerFactory.getLogger(Storage.class);
|
||||
public static final String DIR_KEY = "storage.dir";
|
||||
|
||||
private static DataBaseCorruptionHandler databaseCorruptionHandler;
|
||||
|
||||
public static void setDatabaseCorruptionHandler(DataBaseCorruptionHandler databaseCorruptionHandler) {
|
||||
Storage.databaseCorruptionHandler = databaseCorruptionHandler;
|
||||
}
|
||||
|
||||
public interface DataBaseCorruptionHandler {
|
||||
void onFileCorrupted(String fileName);
|
||||
}
|
||||
|
||||
private final File dir;
|
||||
private FileManager<T> fileManager;
|
||||
private File storageFile;
|
||||
private T serializable;
|
||||
private String fileName;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public Storage(@Named(DIR_KEY) File dir) {
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T initAndGetPersisted(T serializable) {
|
||||
return initAndGetPersisted(serializable, serializable.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T initAndGetPersisted(T serializable, String fileName) {
|
||||
this.serializable = serializable;
|
||||
this.fileName = fileName;
|
||||
storageFile = new File(dir, fileName);
|
||||
fileManager = new FileManager<>(dir, storageFile, 600, TimeUnit.MILLISECONDS);
|
||||
|
||||
return getPersisted(serializable);
|
||||
}
|
||||
|
||||
// Save delayed and on a background thread
|
||||
public void queueUpForSave() {
|
||||
log.debug("save " + fileName);
|
||||
checkNotNull(storageFile, "storageFile = null. Call setupFileStorage before using read/write.");
|
||||
|
||||
fileManager.saveLater(serializable);
|
||||
}
|
||||
|
||||
public void remove(String fileName) {
|
||||
fileManager.removeFile(fileName);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// We do the file read on the UI thread to avoid problems from multi threading.
|
||||
// Data are small and read is done only at startup, so it is no performance issue.
|
||||
@Nullable
|
||||
private T getPersisted(T serializable) {
|
||||
if (storageFile.exists()) {
|
||||
long now = System.currentTimeMillis();
|
||||
try {
|
||||
T persistedObject = fileManager.read(storageFile);
|
||||
log.info("Read {} completed in {}msec", serializable.getClass().getSimpleName(), System.currentTimeMillis() - now);
|
||||
|
||||
// If we did not get any exception we can be sure the data are consistent so we make a backup
|
||||
now = System.currentTimeMillis();
|
||||
fileManager.backupFile(fileName);
|
||||
log.info("Backup {} completed in {}msec", serializable.getClass().getSimpleName(), System.currentTimeMillis() - now);
|
||||
|
||||
return persistedObject;
|
||||
} catch (ClassCastException | IOException e) {
|
||||
e.printStackTrace();
|
||||
log.error("Version of persisted class has changed. We cannot read the persisted data anymore. We make a backup and remove the inconsistent " +
|
||||
"file.");
|
||||
try {
|
||||
// In case the persisted data have been critical (keys) we keep a backup which might be used for recovery
|
||||
fileManager.removeAndBackupFile(fileName);
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
log.error(e1.getMessage());
|
||||
// We swallow Exception if backup fails
|
||||
}
|
||||
databaseCorruptionHandler.onFileCorrupted(storageFile.getName());
|
||||
} catch (Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
log.error(throwable.getMessage());
|
||||
Throwables.propagate(throwable);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue