From d8ae8a65b8b8042342a9cb610ba0383a6386ad53 Mon Sep 17 00:00:00 2001 From: kske Date: Sun, 20 Sep 2020 11:12:33 +0200 Subject: [PATCH] Make LocalDB thread safe and simplify its API --- .../main/java/envoy/client/data/LocalDB.java | 149 ++++++++---------- .../main/java/envoy/client/ui/Startup.java | 9 -- 2 files changed, 66 insertions(+), 92 deletions(-) diff --git a/client/src/main/java/envoy/client/data/LocalDB.java b/client/src/main/java/envoy/client/data/LocalDB.java index f058c10..faee4fd 100644 --- a/client/src/main/java/envoy/client/data/LocalDB.java +++ b/client/src/main/java/envoy/client/data/LocalDB.java @@ -30,59 +30,55 @@ import dev.kske.eventbus.EventListener; */ public final class LocalDB implements EventListener { + // Data private User user; - private Map users = new HashMap<>(); - private List chats = new ArrayList<>(); + private Map users = Collections.synchronizedMap(new HashMap<>()); + private List chats = Collections.synchronizedList(new ArrayList<>()); private IDGenerator idGenerator; private CacheMap cacheMap = new CacheMap(); - private Instant lastSync = Instant.EPOCH; private String authToken; - private File dbDir, userFile, idGeneratorFile, lastLoginFile, usersFile; - private FileLock instanceLock; + + // State management + private Instant lastSync = Instant.EPOCH; + + // Persistence + private File dbDir, userFile, idGeneratorFile, lastLoginFile, usersFile; + private FileLock instanceLock; /** * Constructs an empty local database. To serialize any user-specific data to - * the file system, call {@link LocalDB#initializeUserStorage()} first - * and then {@link LocalDB#save(boolean)}. + * the file system, call {@link LocalDB#save(boolean)}. * * @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 */ - public LocalDB(File dbDir) throws IOException { + public LocalDB(File dbDir) throws IOException, EnvoyException { this.dbDir = dbDir; EventBus.getInstance().registerListener(this); // Ensure that the database directory exists if (!dbDir.exists()) { dbDir.mkdirs(); - } else if (!dbDir.isDirectory()) - throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath())); + } else if (!dbDir.isDirectory()) throw new IOException(String.format("LocalDBDir '%s' is not a directory!", dbDir.getAbsolutePath())); + // Lock the directory + lock(); // Initialize global files idGeneratorFile = new File(dbDir, "id_gen.db"); lastLoginFile = new File(dbDir, "last_login.db"); usersFile = new File(dbDir, "users.db"); + // Load global files + loadGlobalData(); + // Initialize offline caches cacheMap.put(Message.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 * a lock file. @@ -91,9 +87,8 @@ public final class LocalDB implements EventListener { * @throws EnvoyException if the lock cannot by acquired * @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.deleteOnExit(); try { FileChannel fc = FileChannel.open(file.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE); instanceLock = fc.tryLock(); @@ -104,37 +99,24 @@ public final class LocalDB implements EventListener { } /** - * Stores all users. If the client user is specified, their chats will be stored - * as well. The message id generator will also be saved if present. + * Loads the local user registry {@code users.db}, the id generator + * {@code id_gen.db} and last login file {@code last_login.db}. * - * @param isOnline determines which {@code lastSync} time stamp is saved - * @throws IOException if the saving process failed - * @since Envoy Client v0.3-alpha + * @since Envoy Client v0.2-beta */ - public 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); + private synchronized void loadGlobalData() { + try { + try (var in = new ObjectInputStream(new FileInputStream(usersFile))) { + users = (Map) in.readObject(); + } + idGenerator = SerializationUtils.read(idGeneratorFile, IDGenerator.class); + try (var in = new ObjectInputStream(new FileInputStream(lastLoginFile))) { + user = (User) in.readObject(); + authToken = (String) in.readObject(); + } + } catch (IOException | ClassNotFoundException e) {} } - /** - * 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. * @@ -142,36 +124,15 @@ public final class LocalDB implements EventListener { * @throws IOException if the loading process failed * @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))) { - chats = (ArrayList) in.readObject(); + chats = (List) in.readObject(); cacheMap = (CacheMap) in.readObject(); lastSync = (Instant) in.readObject(); } - } - - /** - * 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) {} + synchronize(); } /** @@ -180,7 +141,7 @@ public final class LocalDB implements EventListener { * * @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)); users.put(user.getName(), user); @@ -197,11 +158,33 @@ public final class LocalDB implements EventListener { .forEach(chats::add); } - @Event - private void onNewAuthToken(NewAuthToken evt) { - authToken = evt.get(); + /** + * Stores all users. If the client user is specified, their chats will be stored + * 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} of all users stored locally with their * user names as keys diff --git a/client/src/main/java/envoy/client/ui/Startup.java b/client/src/main/java/envoy/client/ui/Startup.java index 90e4499..fbfdcd1 100644 --- a/client/src/main/java/envoy/client/ui/Startup.java +++ b/client/src/main/java/envoy/client/ui/Startup.java @@ -70,7 +70,6 @@ public final class Startup extends Application { File localDBDir = new File(config.getHomeDirectory(), config.getLocalDB().getPath()); logger.info("Initializing LocalDB at " + localDBDir); localDB = new LocalDB(localDBDir); - localDB.lock(); } catch (IOException | EnvoyException e) { logger.log(Level.SEVERE, "Could not initialize local database: ", e); new Alert(AlertType.ERROR, "Could not initialize local database!\n" + e).showAndWait(); @@ -79,8 +78,6 @@ public final class Startup extends Application { } // Prepare handshake - localDB.loadIDGenerator(); - localDB.loadLastLogin(); context.setLocalDB(localDB); // Configure stage @@ -94,7 +91,6 @@ public final class Startup extends Application { // Authenticate with token if present if (localDB.getAuthToken() != null) { logger.info("Attempting authentication with token..."); - localDB.initializeUserStorage(); localDB.loadUserData(); if (!performHandshake( LoginCredentials.loginWithToken(localDB.getUser().getName(), localDB.getAuthToken(), VERSION, localDB.getLastSync()))) @@ -146,7 +142,6 @@ public final class Startup extends Application { public static boolean attemptOfflineMode(String identifier) { try { // Try entering offline mode - localDB.loadUsers(); final User clientUser = localDB.getUsers().get(identifier); if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown"); client.setSender(clientUser); @@ -169,9 +164,7 @@ public final class Startup extends Application { */ public static Instant loadLastSync(String identifier) { try { - localDB.loadUsers(); localDB.setUser(localDB.getUsers().get(identifier)); - localDB.initializeUserStorage(); localDB.loadUserData(); } catch (final Exception e) { // User storage empty, wrong user name etc. -> default lastSync @@ -186,7 +179,6 @@ public final class Startup extends Application { // Initialize chats in local database try { - localDB.initializeUserStorage(); localDB.loadUserData(); } catch (final FileNotFoundException e) { // The local database file has not yet been created, probably first login @@ -196,7 +188,6 @@ public final class Startup extends Application { } context.initWriteProxy(); - localDB.synchronize(); if (client.isOnline()) context.getWriteProxy().flushCache(); else