]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.acorn/src/org/simantics/acorn/MainState.java
Removed contact application support prints
[simantics/platform.git] / bundles / org.simantics.acorn / src / org / simantics / acorn / MainState.java
1 package org.simantics.acorn;
2
3 import java.io.FileNotFoundException;
4 import java.io.IOException;
5 import java.io.Serializable;
6 import java.nio.file.FileVisitResult;
7 import java.nio.file.Files;
8 import java.nio.file.Path;
9 import java.nio.file.SimpleFileVisitor;
10 import java.nio.file.attribute.BasicFileAttributes;
11 import java.time.ZonedDateTime;
12 import java.time.format.DateTimeFormatter;
13 import java.util.List;
14 import java.util.function.Predicate;
15 import java.util.stream.Collectors;
16 import java.util.stream.Stream;
17
18 import org.simantics.databoard.Bindings;
19 import org.simantics.databoard.binding.mutable.MutableVariant;
20 import org.simantics.databoard.util.binary.BinaryMemory;
21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory;
23
24 public class MainState implements Serializable {
25
26     private static final long serialVersionUID = 6237383147637270225L;
27
28     private static final Logger LOGGER = LoggerFactory.getLogger(MainState.class);
29
30     public static final String MAIN_STATE = "main.state";
31
32     public int headDir;
33
34     public MainState() {
35         this.headDir = 0;
36     }
37
38     private MainState(int headDir) {
39         this.headDir = headDir;
40     }
41     
42     public boolean isInitial() {
43         return this.headDir == 0;
44     }
45
46     public static MainState load(Path directory, Runnable rollbackCallback) throws IOException {
47         Files.createDirectories(directory);
48         Path mainState = directory.resolve(MAIN_STATE);
49         try {
50             MainState state = (MainState) org.simantics.databoard.Files.readFile(
51                     mainState.toFile(),
52                     Bindings.getBindingUnchecked(MainState.class));
53             int latestRevision = state.headDir - 1;
54             try {
55                 if (HeadState.validateHeadStateIntegrity(directory.resolve(latestRevision + "/" + HeadState.HEAD_STATE))) {
56                     archiveRevisionDirectories(directory, latestRevision, rollbackCallback);
57                     return state;
58                 }
59                 LOGGER.warn("Failed to start database from revision " + latestRevision + " stored in " + mainState + ". " + HeadState.HEAD_STATE + " is invalid.");
60                 return rollback(directory, rollbackCallback);
61             } catch (FileNotFoundException e) {
62                 LOGGER.warn("Failed to start database from revision " + latestRevision + " stored in " + mainState + ". Revision does not contain " + HeadState.HEAD_STATE + ".");
63                 return rollback(directory, rollbackCallback);
64             }
65         } catch (FileNotFoundException e) {
66             // The database may also be totally empty at this point
67             if (listRevisionDirs(directory, true, MainState::isInteger).isEmpty())
68                 return new MainState(0);
69
70             LOGGER.warn("Unclean exit detected, " + mainState + " not found. Initiating automatic rollback.");
71             return rollback(directory, rollbackCallback);
72         } catch (Exception e) {
73             LOGGER.warn("Unclean exit detected. Initiating automatic rollback.", e);
74             return rollback(directory, rollbackCallback);
75         } finally {
76             Files.deleteIfExists(mainState);
77         }
78     }
79
80     private static MainState rollback(Path directory, Runnable rollbackCallback) throws IOException {
81         LOGGER.warn("Database rollback initiated for " + directory);
82         rollbackCallback.run();
83         Path latest = findNewHeadStateDir(directory);
84         int latestRevision = latest != null ? safeParseInt(-1, latest) : -1;
85         // +1 because we want to return the next head version to use,
86         // not the latest existing version.
87         MainState state = new MainState( latestRevision + 1 );
88         archiveRevisionDirectories(directory, latestRevision, rollbackCallback);
89         LOGGER.warn("Database rollback completed. Restarting database from revision " + latest);
90         return state;
91     }
92
93     private byte[] toByteArray() throws IOException {
94         try (BinaryMemory rf = new BinaryMemory(4096)) {
95             Bindings.getSerializerUnchecked(Bindings.VARIANT).serialize(rf, MutableVariant.ofInstance(this));
96             return rf.toByteBuffer().array();
97         }
98     }
99
100     public void save(Path directory) throws IOException {
101         Path f = directory.resolve(MAIN_STATE);
102         Files.write(f, toByteArray());
103         FileIO.syncPath(f);
104     }
105
106     private static int safeParseInt(int defaultValue, Path p) {
107         try {
108             return Integer.parseInt(p.getFileName().toString());
109         } catch (NumberFormatException e) {
110             return defaultValue;
111         }
112     }
113
114     private static boolean isInteger(Path p) {
115         return safeParseInt(Integer.MIN_VALUE, p) != Integer.MIN_VALUE;
116     }
117
118     private static Predicate<Path> isGreaterThan(int i) {
119         return p -> {
120             int pi = safeParseInt(Integer.MIN_VALUE, p);
121             return pi != Integer.MIN_VALUE && pi > i;
122         };
123     }
124
125     /**
126      *  
127      * @param directory
128      * @param callback 
129      * @return
130      * @throws IOException
131      */
132     private static Path findNewHeadStateDir(Path directory) throws IOException {
133         for (Path last : listRevisionDirs(directory, true, MainState::isInteger))
134             if (HeadState.validateHeadStateIntegrity(last.resolve(HeadState.HEAD_STATE)))
135                 return last;
136         return null;
137     }
138
139     private static void archiveRevisionDirectories(Path directory, int greaterThanRevision, Runnable rollbackCallback) throws IOException {
140         List<Path> reverseSortedPaths = listRevisionDirs(directory, true, isGreaterThan(greaterThanRevision));
141         if (reverseSortedPaths.isEmpty())
142             return;
143
144         // If none of the revisions to be archived are actually committed revisions
145         // then just delete them. Commitment is indicated by the head.state file.
146         if (!anyContainsHeadState(reverseSortedPaths)) {
147             for (Path p : reverseSortedPaths) {
148                 deleteAll(p);
149                 LOGGER.info("Removed useless working folder " + p);
150             }
151             return;
152         }
153
154         // Some kind of rollback is being performed. There is a possibility that
155         // indexes and virtual graphs are out of sync with the persistent database.
156         rollbackCallback.run();
157
158         Path recoveryFolder = getRecoveryFolder(directory);
159         Files.createDirectories(recoveryFolder);
160         LOGGER.info("Created new database recovery folder " + recoveryFolder);
161         for (Path p : reverseSortedPaths) {
162             Files.move(p, recoveryFolder.resolve(p.getFileName().toString()));
163             LOGGER.info("Archived revision " + p + " in recovery folder " + recoveryFolder);
164         }
165     }
166
167     private static boolean anyContainsHeadState(List<Path> paths) {
168         for (Path p : paths)
169             if (Files.exists(p.resolve(HeadState.HEAD_STATE)))
170                 return true;
171         return false;
172     }
173
174     @SafeVarargs
175     private static List<Path> listRevisionDirs(Path directory, boolean descending, Predicate<Path>... filters) throws IOException {
176         int coef = descending ? -1 : 1;
177         try (Stream<Path> s = Files.walk(directory, 1)) {
178             Stream<Path> fs = s.filter(p -> !p.equals(directory));
179             for (Predicate<Path> p : filters)
180                 fs = fs.filter(p);
181             return fs.filter(Files::isDirectory)
182                     .sorted((p1, p2) -> coef * Integer.compare(Integer.parseInt(p1.getFileName().toString()),
183                                                                Integer.parseInt(p2.getFileName().toString())))
184                     .collect(Collectors.toList());
185         }
186     }
187
188     private static void deleteAll(Path dir) throws IOException {
189         Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
190             @Override
191             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
192                 Files.delete(file);
193                 return FileVisitResult.CONTINUE;
194             }
195             @Override
196             public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
197                 Files.delete(dir);
198                 return FileVisitResult.CONTINUE;
199             }
200         });
201     }
202
203     private static final DateTimeFormatter RECOVERY_DIR_FORMAT = DateTimeFormatter.ofPattern("yyyy-M-d_HH-mm-ss");
204
205     private static Path getRecoveryFolder(Path directory) {
206         return findNonexistentDir(
207                 directory.resolve("recovery"),
208                 RECOVERY_DIR_FORMAT.format(ZonedDateTime.now()));
209     }
210
211     private static Path findNonexistentDir(Path inDirectory, String prefix) {
212         for (int i = 0;; ++i) {
213             Path dir = inDirectory.resolve(i == 0 ? prefix : prefix + "-" + i);
214             if (Files.notExists(dir))
215                 return dir;
216         }
217     }
218
219 }