add local storage for seed map

This commit is contained in:
Manfred Karrer 2015-10-28 22:57:34 +01:00
parent 9ef8b42509
commit 0d9e0d0f31
9 changed files with 34 additions and 23 deletions

View file

@ -17,6 +17,6 @@
package io.bitsquare.common.handlers;
public interface ResultHandler extends Runnable {
public interface ResultHandler {
void handleResult();
}

View 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();
}
}
}

View 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;
}
}