]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.acorn/src/org/simantics/acorn/MainState.java
MainProgram polls nanoTime too often
[simantics/platform.git] / bundles / org.simantics.acorn / src / org / simantics / acorn / MainState.java
index 77335289d73b446b6f10b20a0f1a03ce930ce4f0..f5644c2aeda95b479ffab6f2331dc8c775a88f0e 100644 (file)
 package org.simantics.acorn;
 
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.io.Serializable;
+import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-import org.simantics.databoard.file.RuntimeIOException;
-import org.simantics.utils.FileUtils;
+import org.simantics.databoard.Bindings;
+import org.simantics.databoard.binding.mutable.MutableVariant;
+import org.simantics.databoard.util.binary.BinaryMemory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class MainState implements Serializable {
 
     private static final long serialVersionUID = 6237383147637270225L;
 
-    public int headDir = 0;
+    private static final Logger LOGGER = LoggerFactory.getLogger(MainState.class);
+
+    public static final String MAIN_STATE = "main.state";
+
+    public int headDir;
 
     public MainState() {
+        this.headDir = 0;
     }
 
-    public MainState(int headDir) {
+    private MainState(int headDir) {
         this.headDir = headDir;
     }
+    
+    public boolean isInitial() {
+       return this.headDir == 0;
+    }
 
-    public static MainState load(Path directory) throws IOException {
+    public static MainState load(Path directory, Runnable rollbackCallback) throws IOException {
         Files.createDirectories(directory);
-        Path f = directory.resolve("main.state");
+        Path mainState = directory.resolve(MAIN_STATE);
         try {
-            MainState state = null;
-            try (ObjectInputStream in = new ObjectInputStream(new BufferedInputStream(Files.newInputStream(f)))) {
-                state = (MainState) in.readObject();
-            }
-            while (true) {
-                Path last = directory.resolve(Integer.toString(state.headDir - 1));
-                try {
-                    Path headState = last.resolve("head.state");
-                    HeadState.validateHeadStateIntegrity(headState);
-                    break;
-                } catch (InvalidHeadStateException e) {
-                    e.printStackTrace();
-                    state.headDir--;
-                    uncheckedDeleteAll(last);
+            MainState state = (MainState) org.simantics.databoard.Files.readFile(
+                    mainState.toFile(),
+                    Bindings.getBindingUnchecked(MainState.class));
+            int latestRevision = state.headDir - 1;
+            try {
+                if (HeadState.validateHeadStateIntegrity(directory.resolve(latestRevision + "/" + HeadState.HEAD_STATE))) {
+                    archiveRevisionDirectories(directory, latestRevision, rollbackCallback);
+                    return state;
                 }
+                LOGGER.warn("Failed to start database from revision " + latestRevision + " stored in " + mainState + ". " + HeadState.HEAD_STATE + " is invalid.");
+                return rollback(directory, rollbackCallback);
+            } catch (FileNotFoundException e) {
+                LOGGER.warn("Failed to start database from revision " + latestRevision + " stored in " + mainState + ". Revision does not contain " + HeadState.HEAD_STATE + ".");
+                return rollback(directory, rollbackCallback);
             }
-            return state;
-        } catch(IOException i) {
-            return new MainState( findNewHeadState(directory) );
-        } catch(ClassNotFoundException c) {
-            throw new Error("MainState class not found", c);
+        } catch (FileNotFoundException e) {
+            // The database may also be totally empty at this point
+            if (listRevisionDirs(directory, true, MainState::isInteger).isEmpty())
+                return new MainState(0);
+
+            LOGGER.warn("Unclean exit detected, " + mainState + " not found. Initiating automatic rollback.");
+            return rollback(directory, rollbackCallback);
+        } catch (Exception e) {
+            LOGGER.warn("Unclean exit detected. Initiating automatic rollback.", e);
+            return rollback(directory, rollbackCallback);
         } finally {
-            if (Files.exists(f)) {
-                Files.delete(f);
-            }
+            Files.deleteIfExists(mainState);
         }
     }
 
-    public void save(Path directory) throws IOException {
-        Path f = directory.resolve("main.state");
-        try (ObjectOutputStream out = new ObjectOutputStream(new BufferedOutputStream(Files.newOutputStream(f)))) {
-            out.writeObject(this);
+    private static MainState rollback(Path directory, Runnable rollbackCallback) throws IOException {
+        LOGGER.warn("Database rollback initiated for " + directory);
+        rollbackCallback.run();
+        Path latest = findNewHeadStateDir(directory);
+        int latestRevision = latest != null ? safeParseInt(-1, latest) : -1;
+        // +1 because we want to return the next head version to use,
+        // not the latest existing version.
+        MainState state = new MainState( latestRevision + 1 );
+        archiveRevisionDirectories(directory, latestRevision, rollbackCallback);
+        LOGGER.warn("Database rollback completed. Restarting database from revision " + latest);
+        return state;
+    }
+
+    private byte[] toByteArray() throws IOException {
+        try (BinaryMemory rf = new BinaryMemory(4096)) {
+            Bindings.getSerializerUnchecked(Bindings.VARIANT).serialize(rf, MutableVariant.ofInstance(this));
+            return rf.toByteBuffer().array();
         }
+    }
+
+    public void save(Path directory) throws IOException {
+        Path f = directory.resolve(MAIN_STATE);
+        Files.write(f, toByteArray());
         FileIO.syncPath(f);
     }
 
-    private static boolean isInteger(Path p) {
+    private static int safeParseInt(int defaultValue, Path p) {
         try {
-            Integer.parseInt(p.getFileName().toString());
-            return true;
+            return Integer.parseInt(p.getFileName().toString());
         } catch (NumberFormatException e) {
-            return false;
+            return defaultValue;
         }
     }
 
+    private static boolean isInteger(Path p) {
+        return safeParseInt(Integer.MIN_VALUE, p) != Integer.MIN_VALUE;
+    }
+
+    private static Predicate<Path> isGreaterThan(int i) {
+        return p -> {
+            int pi = safeParseInt(Integer.MIN_VALUE, p);
+            return pi != Integer.MIN_VALUE && pi > i;
+        };
+    }
+
     /**
-     * TODO> shouldn't do two things in the same function, this does both head.state search and directory cleanup
      *  
      * @param directory
+     * @param callback 
      * @return
      * @throws IOException
      */
-    private static int findNewHeadState(Path directory) throws IOException {
-        try (Stream<Path> s = Files.walk(directory, 1)) {
-            List<Path> reverseSortedPaths = s
-            .filter(p -> !p.equals(directory) && isInteger(p) && Files.isDirectory(p))
-            .sorted((p1, p2) -> {
-                int p1Name = Integer.parseInt(p1.getFileName().toString()); 
-                int p2Name = Integer.parseInt(p2.getFileName().toString());
-                return Integer.compare(p2Name, p1Name);
-            }).collect(Collectors.toList());
-
-            int largest = -1;
-            for (Path last : reverseSortedPaths) {
-                Path headState = last.resolve("head.state");
-                if (Files.exists(headState)) {
-                    try {
-                        HeadState.validateHeadStateIntegrity(headState);
-                        largest = safeParseInt(-1, last.getFileName().toString());
-                        break;
-                    } catch (IOException | InvalidHeadStateException e) {
-                        e.printStackTrace();
-                        uncheckedDeleteAll(last);
-                    }
-                } else {
-                    uncheckedDeleteAll(last);
-                }
+    private static Path findNewHeadStateDir(Path directory) throws IOException {
+        for (Path last : listRevisionDirs(directory, true, MainState::isInteger))
+            if (HeadState.validateHeadStateIntegrity(last.resolve(HeadState.HEAD_STATE)))
+                return last;
+        return null;
+    }
+
+    private static void archiveRevisionDirectories(Path directory, int greaterThanRevision, Runnable rollbackCallback) throws IOException {
+        List<Path> reverseSortedPaths = listRevisionDirs(directory, true, isGreaterThan(greaterThanRevision));
+        if (reverseSortedPaths.isEmpty())
+            return;
+
+        // If none of the revisions to be archived are actually committed revisions
+        // then just delete them. Commitment is indicated by the head.state file.
+        if (!anyContainsHeadState(reverseSortedPaths)) {
+            for (Path p : reverseSortedPaths) {
+                deleteAll(p);
+                LOGGER.info("Removed useless working folder " + p);
             }
-            // +1 because we want to return the next head version to use,
-            // not the latest existing version.
-            return largest + 1;
+            return;
+        }
+
+        // Some kind of rollback is being performed. There is a possibility that
+        // indexes and virtual graphs are out of sync with the persistent database.
+        rollbackCallback.run();
+
+        Path recoveryFolder = getRecoveryFolder(directory);
+        Files.createDirectories(recoveryFolder);
+        LOGGER.info("Created new database recovery folder " + recoveryFolder);
+        for (Path p : reverseSortedPaths) {
+            Files.move(p, recoveryFolder.resolve(p.getFileName().toString()));
+            LOGGER.info("Archived revision " + p + " in recovery folder " + recoveryFolder);
         }
     }
 
-    private static int safeParseInt(int defaultValue, String s) {
-        try {
-            return Integer.parseInt(s);
-        } catch (NumberFormatException e) {
-            return defaultValue;
+    private static boolean anyContainsHeadState(List<Path> paths) {
+        for (Path p : paths)
+            if (Files.exists(p.resolve(HeadState.HEAD_STATE)))
+                return true;
+        return false;
+    }
+
+    @SafeVarargs
+    private static List<Path> listRevisionDirs(Path directory, boolean descending, Predicate<Path>... filters) throws IOException {
+        int coef = descending ? -1 : 1;
+        try (Stream<Path> s = Files.walk(directory, 1)) {
+            Stream<Path> fs = s.filter(p -> !p.equals(directory));
+            for (Predicate<Path> p : filters)
+                fs = fs.filter(p);
+            return fs.filter(Files::isDirectory)
+                    .sorted((p1, p2) -> coef * Integer.compare(Integer.parseInt(p1.getFileName().toString()),
+                                                               Integer.parseInt(p2.getFileName().toString())))
+                    .collect(Collectors.toList());
         }
     }
 
-    private static void uncheckedDeleteAll(Path path) {
-        try {
-            FileUtils.deleteAll(path.toFile());
-        } catch (IOException e) {
-            throw new RuntimeIOException(e);
+    private static void deleteAll(Path dir) throws IOException {
+        Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                Files.delete(file);
+                return FileVisitResult.CONTINUE;
+            }
+            @Override
+            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+                Files.delete(dir);
+                return FileVisitResult.CONTINUE;
+            }
+        });
+    }
+
+    private static final DateTimeFormatter RECOVERY_DIR_FORMAT = DateTimeFormatter.ofPattern("yyyy-M-d_HH-mm-ss");
+
+    private static Path getRecoveryFolder(Path directory) {
+        return findNonexistentDir(
+                directory.resolve("recovery"),
+                RECOVERY_DIR_FORMAT.format(ZonedDateTime.now()));
+    }
+
+    private static Path findNonexistentDir(Path inDirectory, String prefix) {
+        for (int i = 0;; ++i) {
+            Path dir = inDirectory.resolve(i == 0 ? prefix : prefix + "-" + i);
+            if (Files.notExists(dir))
+                return dir;
         }
     }