]> gerrit.simantics Code Review - simantics/platform.git/commitdiff
Merge "(refs #6924) Support for record field access syntax."
authorHannu Niemistö <hannu.niemisto@semantum.fi>
Thu, 27 Apr 2017 20:55:55 +0000 (23:55 +0300)
committerGerrit Code Review <gerrit2@www.simantics.org>
Thu, 27 Apr 2017 20:55:55 +0000 (23:55 +0300)
bundles/org.simantics.acorn/src/org/simantics/acorn/ClusterManager.java
bundles/org.simantics.acorn/src/org/simantics/acorn/MainState.java
bundles/org.simantics.databoard/src/org/simantics/databoard/Files.java
bundles/org.simantics.modeling.ui/src/org/simantics/modeling/ui/diagramEditor/DisposingPolicy.java

index 40c5de37e8c1991e6e5ce846e2b90e7b651b6c5e..e0bcbebf7421906a61cd13ff626719aac0f00ef0 100644 (file)
@@ -2,7 +2,6 @@ package org.simantics.acorn;
 
 import java.io.IOException;
 import java.math.BigInteger;
-import java.nio.file.CopyOption;
 import java.nio.file.DirectoryStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -24,7 +23,6 @@ import org.simantics.acorn.lru.ClusterLRU;
 import org.simantics.acorn.lru.ClusterStreamChunk;
 import org.simantics.acorn.lru.FileInfo;
 import org.simantics.acorn.lru.LRU;
-import org.simantics.databoard.file.RuntimeIOException;
 import org.simantics.db.ClusterCreator;
 import org.simantics.db.Database.Session.ClusterIds;
 import org.simantics.db.Database.Session.ResourceSegment;
@@ -405,7 +403,7 @@ public class ClusterManager {
        public void load() throws IOException {
 
                // Main state
-               mainState = MainState.load(dbFolder, t -> rollback.set(true));
+               mainState = MainState.load(dbFolder, () -> rollback.set(true));
 
                lastSessionDirectory = dbFolder.resolve(Integer.toString(mainState.headDir - 1));
                
index 7d158042173089a2034a3b2e5953fc1423fb6e7c..63fc5d4937b06cd0aff10b571506e66830741535 100644 (file)
 package org.simantics.acorn;
 
-import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
-import java.io.OutputStream;
 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.Consumer;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.simantics.acorn.exception.InvalidHeadStateException;
 import org.simantics.databoard.Bindings;
 import org.simantics.databoard.binding.mutable.MutableVariant;
-import org.simantics.databoard.file.RuntimeIOException;
-import org.simantics.databoard.serialization.Serializer;
 import org.simantics.databoard.util.binary.BinaryMemory;
-import org.simantics.utils.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class MainState implements Serializable {
 
     private static final long serialVersionUID = 6237383147637270225L;
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(MainState.class);
+
     public static final String MAIN_STATE = "main.state";
-    
-    public int headDir = 0;
+
+    public int headDir;
 
     public MainState() {
+        this.headDir = 0;
     }
-    
+
     private MainState(int headDir) {
         this.headDir = headDir;
     }
 
-    public static MainState load(Path directory, Consumer<Exception> callback) throws IOException {
+    public static MainState load(Path directory, Runnable rollbackCallback) throws IOException {
         Files.createDirectories(directory);
         Path mainState = directory.resolve(MAIN_STATE);
         try {
-            byte[] bytes = Files.readAllBytes(mainState);
-            MainState state = null;
-            try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes)) {
-                state = (MainState) org.simantics.databoard.Files.readFile(bais, Bindings.getBindingUnchecked(MainState.class));
+            MainState state = (MainState) org.simantics.databoard.Files.readFile(
+                    mainState.toFile(),
+                    Bindings.getBindingUnchecked(MainState.class));
+            int latestRevision = state.headDir - 1;
+            try {
+                HeadState.validateHeadStateIntegrity(directory.resolve(latestRevision + "/" + HeadState.HEAD_STATE));
+                archiveRevisionDirectories(directory, latestRevision, rollbackCallback);
+                return state;
+            } catch (InvalidHeadStateException e) {
+                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);
             }
-            
-            while (true) {
-                Path latest = directory.resolve(Integer.toString(state.headDir - 1));
-                try {
-                    Path headState = latest.resolve(HeadState.HEAD_STATE);
-                    HeadState.validateHeadStateIntegrity(headState);
-                    break;
-                } catch (InvalidHeadStateException e) {
-                    e.printStackTrace();
-                    state.headDir--;
-                    callback.accept(e);
-                } finally {
-                    cleanBaseDirectory(directory, latest, callback);
-                }
-            }
-            return state;
-        } catch(Exception i) {
-            callback.accept(i);
-            int largest = -1;
-            Path latest = findNewHeadStateDir(directory, callback);
-            if (latest != null)
-                largest = safeParseInt(-1, latest.getFileName().toString());
-            // +1 because we want to return the next head version to use,
-            // not the latest existing version.
-            largest++;
-            MainState state = new MainState( largest );
-            cleanBaseDirectory(directory, latest, callback);
-            return state;
+        } 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(mainState)) {
-                Files.delete(mainState);
-            }
+            Files.deleteIfExists(mainState);
+        }
+    }
+
+    private static MainState rollback(Path directory, Runnable rollbackCallback) throws IOException {
+        LOGGER.warn("Database rollback initiated for " + directory);
+        rollbackCallback.run();
+        Path latest = findNewHeadStateDir(directory, rollbackCallback);
+        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);
-        BinaryMemory rf = new BinaryMemory(4096);
-        try {
-            MutableVariant v = new MutableVariant(Bindings.getBindingUnchecked(MainState.class), this);
-            Serializer s = Bindings.getSerializerUnchecked( Bindings.VARIANT );
-            s.serialize(rf, v);
-        } finally {
-            rf.close();
-        }
-        byte[] bytes = rf.toByteBuffer().array();
-        try (OutputStream out = Files.newOutputStream(f)) {
-            out.write(bytes);
-        }
+        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;
+        };
+    }
+
     /**
      *  
      * @param directory
@@ -111,70 +126,96 @@ public class MainState implements Serializable {
      * @return
      * @throws IOException
      */
-    private static Path findNewHeadStateDir(Path directory, Consumer<Exception> callback) 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());
-
-            Path latest = null;
-            for (Path last : reverseSortedPaths) {
-                Path headState = last.resolve(HeadState.HEAD_STATE);
-                try {
-                    HeadState.validateHeadStateIntegrity(headState);
-                    latest = last;
-                    break;
-                } catch (IOException | InvalidHeadStateException e) {
-                    // Cleanup is done in {@link cleanBaseDirectory} method
-                    callback.accept(e);
-                }
+    private static Path findNewHeadStateDir(Path directory, Runnable rollbackCallback) throws IOException {
+        for (Path last : listRevisionDirs(directory, true, MainState::isInteger)) {
+            try {
+                HeadState.validateHeadStateIntegrity(last.resolve(HeadState.HEAD_STATE));
+                return last;
+            } catch (IOException | InvalidHeadStateException e) {
+                // Cleanup is done in {@link cleanRevisionDirectories} method
+                rollbackCallback.run();
             }
-            return latest;
         }
+        return null;
     }
 
-    private static int safeParseInt(int defaultValue, String s) {
-        try {
-            return Integer.parseInt(s);
-        } catch (NumberFormatException e) {
-            return defaultValue;
+    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);
+            }
+            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 boolean anyContainsHeadState(List<Path> paths) {
+        for (Path p : paths)
+            if (Files.exists(p.resolve(HeadState.HEAD_STATE)))
+                return true;
+        return false;
     }
 
-    private static void cleanBaseDirectory(Path directory, Path latest, Consumer<Exception> callback) throws IOException {
+    @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)) {
-            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());
-            
-            for (Path p : reverseSortedPaths) {
-                if (!p.equals(latest)) {
-                    if (Files.exists(p.resolve(HeadState.HEAD_STATE))) {
-                        // this indicates that there is a possibility that index and vg's are out of sync
-                        // if we are able to find folders with higher number than the current head.state
-                        callback.accept(null);
-                    }
-                    uncheckedDeleteAll(p);
-                } else {
-                    break;
-                }
-            }
+            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;
         }
     }
 
index ae4f46f31634cf091fc0977db09630abaa9f62d0..cc41e8e9b021254899aa2e942c2f8fc69c81a603 100644 (file)
@@ -186,7 +186,7 @@ public class Files {
         * @throws IOException 
         */
        public static Datatype readFileType(File file) throws IOException {
-               BinaryFile rf = new BinaryFile( file );
+               BinaryFile rf = new BinaryFile( file, "r" );
                try {
                        Binding datatype_binding = Bindings.getBindingUnchecked( Datatype.class );
                        return (Datatype) Bindings.getSerializerUnchecked( datatype_binding ).deserialize( rf );
@@ -206,7 +206,7 @@ public class Files {
         * @throws IOException 
         */
        public static Object readFile(File file, Binding binding) throws IOException {
-               BinaryFile rf = new BinaryFile( file );
+               BinaryFile rf = new BinaryFile( file, "r" );
                try {
                        Binding datatype_binding = Bindings.getBindingUnchecked( Datatype.class );
                        Datatype type = (Datatype) Bindings.getSerializerUnchecked( datatype_binding ).deserialize( rf );
@@ -239,7 +239,7 @@ public class Files {
         * @throws IOException 
         */
        public static void readFile(File file, RecordBinding binding, Object dst) throws IOException {
-               BinaryFile rf = new BinaryFile( file );
+               BinaryFile rf = new BinaryFile( file, "r" );
                try {
                        Binding datatype_binding = Bindings.getBindingUnchecked( Datatype.class );
                        Datatype type = (Datatype) Bindings.getSerializerUnchecked( datatype_binding ).deserialize( rf );
@@ -353,7 +353,7 @@ public class Files {
        
        public static DataInput openInput( File file ) throws IOException
        {
-               return new BinaryFile(file);
+               return new BinaryFile(file, "r");
        }
        
        public static DataInput openInput( byte[] data ) 
index a3cd08b068745f43bc0599fb04968b738b4ef9dc..9e9b7418bce5cbf069e6d512fa2c9c0a1402e481 100644 (file)
@@ -8,34 +8,40 @@ import org.eclipse.swt.widgets.Display;
 
 
 public class DisposingPolicy {
-    
+
     public static final boolean DEBUG = false;
-    
+
     public static final int MAX_QUEUE_LENGTH = 8;
     public static final long DISPOSE_TIME = 30000L; // ms
     public static final long MIN_DELAY = 200L; // ms
-    
+
+    private final int maxQueueLength;
     private ArrayDeque<Runnable> disposerQueue = new ArrayDeque<Runnable>(MAX_QUEUE_LENGTH);
     private TObjectLongHashMap<Runnable> disposeTime =
             new TObjectLongHashMap<Runnable>(MAX_QUEUE_LENGTH);
     private Runnable currentlyScheduled = null;
-    
-    private Runnable disposeOne = new Runnable() {
-        @Override
-        public void run() {
-            if(!disposerQueue.isEmpty()) {
-                Runnable runnable = disposerQueue.removeFirst();
-                disposeTime.remove(runnable);
-                currentlyScheduled = null;
-                runnable.run();
-                if(DEBUG)
-                    System.out.println("Executed disposer " + runnable);
-                if(!disposerQueue.isEmpty())
-                    scheduleDispose();
-            }
+
+    private Runnable disposeOne = () -> {
+        if(!disposerQueue.isEmpty()) {
+            Runnable runnable = disposerQueue.removeFirst();
+            disposeTime.remove(runnable);
+            currentlyScheduled = null;
+            runnable.run();
+            if(DEBUG)
+                System.out.println("Executed disposer " + runnable);
+            if(!disposerQueue.isEmpty())
+                scheduleDispose();
         }
     };
-    
+
+    public DisposingPolicy() {
+       this(MAX_QUEUE_LENGTH);
+    }
+
+    public DisposingPolicy(int maxQueueLength) {
+        this.maxQueueLength = maxQueueLength;
+    }
+
     private void scheduleDispose() {
         currentlyScheduled = disposerQueue.peekFirst();
         long delay = Math.max(
@@ -45,10 +51,10 @@ public class DisposingPolicy {
         if(DEBUG)
             System.out.println("Scheduled disposer " + currentlyScheduled + " in " + delay + " ms");
     }
-    
+
     /**
      * Runs the disposer either after DISPOSE_TIME or when there are
-     * more than MAX_QUEUE_LENGTH disposers active and this is first
+     * more than {@link #maxQueueLength} disposers active and this is first
      * of them (by activation order). This method must be called from
      * UI thread.
      */
@@ -57,14 +63,14 @@ public class DisposingPolicy {
             System.out.println("Added disposer " + disposer);
         if(disposeTime.contains(disposer))
             return;
-        if(disposerQueue.size() >= MAX_QUEUE_LENGTH)
+        if(disposerQueue.size() >= maxQueueLength)
             disposeOne.run();
         disposerQueue.addLast(disposer);
         disposeTime.put(disposer, System.currentTimeMillis()+DISPOSE_TIME);
         if(disposerQueue.size() == 1)
             scheduleDispose();
     }
-    
+
     /**
      * Cancels a disposer added before. This method must be called from
      * UI thread.
@@ -80,5 +86,5 @@ public class DisposingPolicy {
                 scheduleDispose();
         }
     }
-    
+
 }