1 package org.simantics.acorn;
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;
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;
24 public class MainState implements Serializable {
26 private static final long serialVersionUID = 6237383147637270225L;
28 private static final Logger LOGGER = LoggerFactory.getLogger(MainState.class);
30 public static final String MAIN_STATE = "main.state";
38 private MainState(int headDir) {
39 this.headDir = headDir;
42 public boolean isInitial() {
43 return this.headDir == 0;
46 public static MainState load(Path directory, Runnable rollbackCallback) throws IOException {
47 Files.createDirectories(directory);
48 Path mainState = directory.resolve(MAIN_STATE);
50 MainState state = (MainState) org.simantics.databoard.Files.readFile(
52 Bindings.getBindingUnchecked(MainState.class));
53 int latestRevision = state.headDir - 1;
55 if (HeadState.validateHeadStateIntegrity(directory.resolve(latestRevision + "/" + HeadState.HEAD_STATE))) {
56 archiveRevisionDirectories(directory, latestRevision, rollbackCallback);
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);
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);
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);
76 Files.deleteIfExists(mainState);
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);
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();
100 public void save(Path directory) throws IOException {
101 Path f = directory.resolve(MAIN_STATE);
102 Files.write(f, toByteArray());
106 private static int safeParseInt(int defaultValue, Path p) {
108 return Integer.parseInt(p.getFileName().toString());
109 } catch (NumberFormatException e) {
114 private static boolean isInteger(Path p) {
115 return safeParseInt(Integer.MIN_VALUE, p) != Integer.MIN_VALUE;
118 private static Predicate<Path> isGreaterThan(int i) {
120 int pi = safeParseInt(Integer.MIN_VALUE, p);
121 return pi != Integer.MIN_VALUE && pi > i;
130 * @throws IOException
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)))
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())
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) {
149 LOGGER.info("Removed useless working folder " + p);
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();
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);
167 private static boolean anyContainsHeadState(List<Path> paths) {
169 if (Files.exists(p.resolve(HeadState.HEAD_STATE)))
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)
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());
188 private static void deleteAll(Path dir) throws IOException {
189 Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
191 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
193 return FileVisitResult.CONTINUE;
196 public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
198 return FileVisitResult.CONTINUE;
203 private static final DateTimeFormatter RECOVERY_DIR_FORMAT = DateTimeFormatter.ofPattern("yyyy-M-d_HH-mm-ss");
205 private static Path getRecoveryFolder(Path directory) {
206 return findNonexistentDir(
207 directory.resolve("recovery"),
208 RECOVERY_DIR_FORMAT.format(ZonedDateTime.now()));
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))