Merge pull request 'Make LocalDB Thread Safe and Simplify its API' (#38) from refactor-local-db into develop
Reviewed-on: https://git.kske.dev/zdm/envoy/pulls/38 Reviewed-by: delvh <leon@kske.dev>
This commit is contained in:
commit
8ed6faca96
@ -30,59 +30,55 @@ import dev.kske.eventbus.EventListener;
|
|||||||
*/
|
*/
|
||||||
public final class LocalDB implements EventListener {
|
public final class LocalDB implements EventListener {
|
||||||
|
|
||||||
|
// Data
|
||||||
private User user;
|
private User user;
|
||||||
private Map<String, User> users = new HashMap<>();
|
private Map<String, User> users = Collections.synchronizedMap(new HashMap<>());
|
||||||
private List<Chat> chats = new ArrayList<>();
|
private List<Chat> chats = Collections.synchronizedList(new ArrayList<>());
|
||||||
private IDGenerator idGenerator;
|
private IDGenerator idGenerator;
|
||||||
private CacheMap cacheMap = new CacheMap();
|
private CacheMap cacheMap = new CacheMap();
|
||||||
private Instant lastSync = Instant.EPOCH;
|
|
||||||
private String authToken;
|
private String authToken;
|
||||||
|
|
||||||
|
// State management
|
||||||
|
private Instant lastSync = Instant.EPOCH;
|
||||||
|
|
||||||
|
// Persistence
|
||||||
private File dbDir, userFile, idGeneratorFile, lastLoginFile, usersFile;
|
private File dbDir, userFile, idGeneratorFile, lastLoginFile, usersFile;
|
||||||
private FileLock instanceLock;
|
private FileLock instanceLock;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs an empty local database. To serialize any user-specific data to
|
* Constructs an empty local database. To serialize any user-specific data to
|
||||||
* the file system, call {@link LocalDB#initializeUserStorage()} first
|
* the file system, call {@link LocalDB#save(boolean)}.
|
||||||
* and then {@link LocalDB#save(boolean)}.
|
|
||||||
*
|
*
|
||||||
* @param dbDir the directory in which to persist data
|
* @param dbDir the directory in which to persist data
|
||||||
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
* @throws IOException if {@code dbDir} is a file (and not a directory)
|
||||||
|
* @throws EnvoyException if {@code dbDir} is in use by another Envoy instance
|
||||||
* @since Envoy Client v0.2-beta
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public LocalDB(File dbDir) throws IOException {
|
public LocalDB(File dbDir) throws IOException, EnvoyException {
|
||||||
this.dbDir = dbDir;
|
this.dbDir = dbDir;
|
||||||
EventBus.getInstance().registerListener(this);
|
EventBus.getInstance().registerListener(this);
|
||||||
|
|
||||||
// Ensure that the database directory exists
|
// Ensure that the database directory exists
|
||||||
if (!dbDir.exists()) {
|
if (!dbDir.exists()) {
|
||||||
dbDir.mkdirs();
|
dbDir.mkdirs();
|
||||||
} else if (!dbDir.isDirectory())
|
} else if (!dbDir.isDirectory()) throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
||||||
throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath()));
|
|
||||||
|
|
||||||
|
// Lock the directory
|
||||||
|
lock();
|
||||||
|
|
||||||
// Initialize global files
|
// Initialize global files
|
||||||
idGeneratorFile = new File(dbDir, "id_gen.db");
|
idGeneratorFile = new File(dbDir, "id_gen.db");
|
||||||
lastLoginFile = new File(dbDir, "last_login.db");
|
lastLoginFile = new File(dbDir, "last_login.db");
|
||||||
usersFile = new File(dbDir, "users.db");
|
usersFile = new File(dbDir, "users.db");
|
||||||
|
|
||||||
|
// Load global files
|
||||||
|
loadGlobalData();
|
||||||
|
|
||||||
// Initialize offline caches
|
// Initialize offline caches
|
||||||
cacheMap.put(Message.class, new Cache<>());
|
cacheMap.put(Message.class, new Cache<>());
|
||||||
cacheMap.put(MessageStatusChange.class, new Cache<>());
|
cacheMap.put(MessageStatusChange.class, new Cache<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a database file for a user-specific list of chats.
|
|
||||||
*
|
|
||||||
* @throws IllegalStateException if the client user is not specified
|
|
||||||
* @since Envoy Client v0.1-alpha
|
|
||||||
*/
|
|
||||||
public void initializeUserStorage() {
|
|
||||||
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
|
||||||
userFile = new File(dbDir, user.getID() + ".db");
|
|
||||||
}
|
|
||||||
|
|
||||||
FileChannel fc;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensured that only one Envoy instance is using this local database by creating
|
* Ensured that only one Envoy instance is using this local database by creating
|
||||||
* a lock file.
|
* a lock file.
|
||||||
@ -91,9 +87,8 @@ public final class LocalDB implements EventListener {
|
|||||||
* @throws EnvoyException if the lock cannot by acquired
|
* @throws EnvoyException if the lock cannot by acquired
|
||||||
* @since Envoy Client v0.2-beta
|
* @since Envoy Client v0.2-beta
|
||||||
*/
|
*/
|
||||||
public void lock() throws EnvoyException {
|
private synchronized void lock() throws EnvoyException {
|
||||||
File file = new File(dbDir, "instance.lock");
|
File file = new File(dbDir, "instance.lock");
|
||||||
file.deleteOnExit();
|
|
||||||
try {
|
try {
|
||||||
FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
|
FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE);
|
||||||
instanceLock = fc.tryLock();
|
instanceLock = fc.tryLock();
|
||||||
@ -104,36 +99,23 @@ public final class LocalDB implements EventListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores all users. If the client user is specified, their chats will be stored
|
* Loads the local user registry {@code users.db}, the id generator
|
||||||
* as well. The message id generator will also be saved if present.
|
* {@code id_gen.db} and last login file {@code last_login.db}.
|
||||||
*
|
*
|
||||||
* @param isOnline determines which {@code lastSync} time stamp is saved
|
* @since Envoy Client v0.2-beta
|
||||||
* @throws IOException if the saving process failed
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
*/
|
||||||
public void save(boolean isOnline) throws IOException {
|
private synchronized void loadGlobalData() {
|
||||||
|
try {
|
||||||
// Save users
|
try (var in = new ObjectInputStream(new FileInputStream(usersFile))) {
|
||||||
SerializationUtils.write(usersFile, users);
|
users = (Map<String, User>) in.readObject();
|
||||||
|
}
|
||||||
// Save user data and last sync time stamp
|
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
||||||
if (user != null) SerializationUtils.write(userFile, chats, cacheMap, isOnline ? Instant.now() : lastSync);
|
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
|
||||||
|
user = (User) in.readObject();
|
||||||
// Save last login information
|
authToken = (String) in.readObject();
|
||||||
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
|
}
|
||||||
|
} catch (IOException | ClassNotFoundException e) {}
|
||||||
// Save id generator
|
|
||||||
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads all user data.
|
|
||||||
*
|
|
||||||
* @throws ClassNotFoundException if the loading process failed
|
|
||||||
* @throws IOException if the loading process failed
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public void loadUsers() throws ClassNotFoundException, IOException { users = SerializationUtils.read(usersFile, HashMap.class); }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads all data of the client user.
|
* Loads all data of the client user.
|
||||||
@ -142,36 +124,15 @@ public final class LocalDB implements EventListener {
|
|||||||
* @throws IOException if the loading process failed
|
* @throws IOException if the loading process failed
|
||||||
* @since Envoy Client v0.3-alpha
|
* @since Envoy Client v0.3-alpha
|
||||||
*/
|
*/
|
||||||
public void loadUserData() throws ClassNotFoundException, IOException {
|
public synchronized void loadUserData() throws ClassNotFoundException, IOException {
|
||||||
|
if (user == null) throw new IllegalStateException("Client user is null, cannot initialize user storage");
|
||||||
|
userFile = new File(dbDir, user.getID() + ".db");
|
||||||
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
||||||
chats = (ArrayList<Chat>) in.readObject();
|
chats = (List<Chat>) in.readObject();
|
||||||
cacheMap = (CacheMap) in.readObject();
|
cacheMap = (CacheMap) in.readObject();
|
||||||
lastSync = (Instant) in.readObject();
|
lastSync = (Instant) in.readObject();
|
||||||
}
|
}
|
||||||
}
|
synchronize();
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the ID generator. Any exception thrown during this process is ignored.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.3-alpha
|
|
||||||
*/
|
|
||||||
public void loadIDGenerator() {
|
|
||||||
try {
|
|
||||||
idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class);
|
|
||||||
} catch (ClassNotFoundException | IOException e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the last login information. Any exception thrown during this process is
|
|
||||||
* ignored.
|
|
||||||
*
|
|
||||||
* @since Envoy Client v0.2-beta
|
|
||||||
*/
|
|
||||||
public void loadLastLogin() {
|
|
||||||
try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) {
|
|
||||||
user = (User) in.readObject();
|
|
||||||
authToken = (String) in.readObject();
|
|
||||||
} catch (ClassNotFoundException | IOException e) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -180,7 +141,7 @@ public final class LocalDB implements EventListener {
|
|||||||
*
|
*
|
||||||
* @since Envoy Client v0.1-beta
|
* @since Envoy Client v0.1-beta
|
||||||
*/
|
*/
|
||||||
public void synchronize() {
|
private void synchronize() {
|
||||||
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), (User) u));
|
user.getContacts().stream().filter(u -> u instanceof User && !users.containsKey(u.getName())).forEach(u -> users.put(u.getName(), (User) u));
|
||||||
users.put(user.getName(), user);
|
users.put(user.getName(), user);
|
||||||
|
|
||||||
@ -197,11 +158,33 @@ public final class LocalDB implements EventListener {
|
|||||||
.forEach(chats::add);
|
.forEach(chats::add);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Event
|
/**
|
||||||
private void onNewAuthToken(NewAuthToken evt) {
|
* Stores all users. If the client user is specified, their chats will be stored
|
||||||
authToken = evt.get();
|
* as well. The message id generator will also be saved if present.
|
||||||
|
*
|
||||||
|
* @param isOnline determines which {@code lastSync} time stamp is saved
|
||||||
|
* @throws IOException if the saving process failed
|
||||||
|
* @since Envoy Client v0.3-alpha
|
||||||
|
*/
|
||||||
|
public synchronized void save(boolean isOnline) throws IOException {
|
||||||
|
|
||||||
|
// Save users
|
||||||
|
SerializationUtils.write(usersFile, users);
|
||||||
|
|
||||||
|
// Save user data and last sync time stamp
|
||||||
|
if (user != null) SerializationUtils.write(userFile, chats, cacheMap, isOnline ? Instant.now() : lastSync);
|
||||||
|
|
||||||
|
// Save last login information
|
||||||
|
if (authToken != null) SerializationUtils.write(lastLoginFile, user, authToken);
|
||||||
|
|
||||||
|
// Save id generator
|
||||||
|
if (hasIDGenerator()) SerializationUtils.write(idGeneratorFile, idGenerator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Event
|
||||||
|
private void onNewAuthToken(NewAuthToken evt) { authToken = evt.get(); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return a {@code Map<String, User>} of all users stored locally with their
|
* @return a {@code Map<String, User>} of all users stored locally with their
|
||||||
* user names as keys
|
* user names as keys
|
||||||
|
@ -70,7 +70,6 @@ public final class Startup extends Application {
|
|||||||
final var localDBDir = new File(config.getHomeDirectory(), config.getLocalDB().getPath());
|
final var localDBDir = new File(config.getHomeDirectory(), config.getLocalDB().getPath());
|
||||||
logger.info("Initializing LocalDB at " + localDBDir);
|
logger.info("Initializing LocalDB at " + localDBDir);
|
||||||
localDB = new LocalDB(localDBDir);
|
localDB = new LocalDB(localDBDir);
|
||||||
localDB.lock();
|
|
||||||
} catch (IOException | EnvoyException e) {
|
} catch (IOException | EnvoyException e) {
|
||||||
logger.log(Level.SEVERE, "Could not initialize local database: ", e);
|
logger.log(Level.SEVERE, "Could not initialize local database: ", e);
|
||||||
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait();
|
new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait();
|
||||||
@ -79,8 +78,6 @@ public final class Startup extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Prepare handshake
|
// Prepare handshake
|
||||||
localDB.loadIDGenerator();
|
|
||||||
localDB.loadLastLogin();
|
|
||||||
context.setLocalDB(localDB);
|
context.setLocalDB(localDB);
|
||||||
|
|
||||||
// Configure stage
|
// Configure stage
|
||||||
@ -94,7 +91,6 @@ public final class Startup extends Application {
|
|||||||
// Authenticate with token if present
|
// Authenticate with token if present
|
||||||
if (localDB.getAuthToken() != null) {
|
if (localDB.getAuthToken() != null) {
|
||||||
logger.info("Attempting authentication with token...");
|
logger.info("Attempting authentication with token...");
|
||||||
localDB.initializeUserStorage();
|
|
||||||
localDB.loadUserData();
|
localDB.loadUserData();
|
||||||
if (!performHandshake(
|
if (!performHandshake(
|
||||||
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync())))
|
LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync())))
|
||||||
@ -142,7 +138,6 @@ public final class Startup extends Application {
|
|||||||
public static boolean attemptOfflineMode(String identifier) {
|
public static boolean attemptOfflineMode(String identifier) {
|
||||||
try {
|
try {
|
||||||
// Try entering offline mode
|
// Try entering offline mode
|
||||||
localDB.loadUsers();
|
|
||||||
final User clientUser = localDB.getUsers().get(identifier);
|
final User clientUser = localDB.getUsers().get(identifier);
|
||||||
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
|
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
|
||||||
client.setSender(clientUser);
|
client.setSender(clientUser);
|
||||||
@ -165,9 +160,7 @@ public final class Startup extends Application {
|
|||||||
*/
|
*/
|
||||||
public static Instant loadLastSync(String identifier) {
|
public static Instant loadLastSync(String identifier) {
|
||||||
try {
|
try {
|
||||||
localDB.loadUsers();
|
|
||||||
localDB.setUser(localDB.getUsers().get(identifier));
|
localDB.setUser(localDB.getUsers().get(identifier));
|
||||||
localDB.initializeUserStorage();
|
|
||||||
localDB.loadUserData();
|
localDB.loadUserData();
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
// User storage empty, wrong user name etc. -> default lastSync
|
// User storage empty, wrong user name etc. -> default lastSync
|
||||||
@ -182,7 +175,6 @@ public final class Startup extends Application {
|
|||||||
|
|
||||||
// Initialize chats in local database
|
// Initialize chats in local database
|
||||||
try {
|
try {
|
||||||
localDB.initializeUserStorage();
|
|
||||||
localDB.loadUserData();
|
localDB.loadUserData();
|
||||||
} catch (final FileNotFoundException e) {
|
} catch (final FileNotFoundException e) {
|
||||||
// The local database file has not yet been created, probably first login
|
// The local database file has not yet been created, probably first login
|
||||||
@ -192,7 +184,6 @@ public final class Startup extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.initWriteProxy();
|
context.initWriteProxy();
|
||||||
localDB.synchronize();
|
|
||||||
|
|
||||||
if (client.isOnline()) context.getWriteProxy().flushCache();
|
if (client.isOnline()) context.getWriteProxy().flushCache();
|
||||||
else
|
else
|
||||||
|
Reference in New Issue
Block a user