From 228ba2b2e2d1e26e0638c5e75dc28f53c112428b Mon Sep 17 00:00:00 2001 From: James Seibel Date: Mon, 13 Feb 2023 20:03:54 -0600 Subject: [PATCH] Improve corrupted Data File handling --- .../transform/DataRenderTransformer.java | 6 ++- .../com/seibel/lod/core/file/FileUtil.java | 50 ++++++++++++++++++ .../core/file/datafile/DataFileHandler.java | 52 ++++++++++++++----- .../lod/core/file/datafile/DataMetaFile.java | 42 ++++++++------- .../file/renderfile/RenderFileHandler.java | 10 ++-- 5 files changed, 120 insertions(+), 40 deletions(-) create mode 100644 core/src/main/java/com/seibel/lod/core/file/FileUtil.java diff --git a/core/src/main/java/com/seibel/lod/core/datatype/transform/DataRenderTransformer.java b/core/src/main/java/com/seibel/lod/core/datatype/transform/DataRenderTransformer.java index f91bc5a72..0d8a6f964 100644 --- a/core/src/main/java/com/seibel/lod/core/datatype/transform/DataRenderTransformer.java +++ b/core/src/main/java/com/seibel/lod/core/datatype/transform/DataRenderTransformer.java @@ -28,7 +28,11 @@ public class DataRenderTransformer private static ILodRenderSource transform(ILodDataSource dataSource, IDhClientLevel level) { - if (dataSource == null) return null; + if (dataSource == null) + { + return null; + } + return ColumnRenderLoader.LOADER_BY_SOURCE_TYPE.get(ColumnRenderSource.class) .stream().findFirst().get().createRenderSource(dataSource, level); } diff --git a/core/src/main/java/com/seibel/lod/core/file/FileUtil.java b/core/src/main/java/com/seibel/lod/core/file/FileUtil.java new file mode 100644 index 000000000..fc6c140f3 --- /dev/null +++ b/core/src/main/java/com/seibel/lod/core/file/FileUtil.java @@ -0,0 +1,50 @@ +package com.seibel.lod.core.file; + +import com.seibel.lod.core.logging.DhLoggerBuilder; +import org.apache.logging.log4j.Logger; + +import java.io.File; + +public class FileUtil +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + /** + * Renames the given file to FILE_NAME.ORIGINAL_PREFIX.corrupted. + * If an existing corrupted file already exists, this will attempt to remove it first. + * + * @return the file after it has been renamed + */ + public static File renameCorruptedFile(File file) + { + String corruptedFileName = file.getName() + ".corrupted"; + + File corruptedFile = new File(file.getParentFile(), corruptedFileName); + if (corruptedFile.exists()) + { + // could happen if there was a corrupted file before that was removed + if (!corruptedFile.delete()) + { + LOGGER.error("Unable to delete pre-existing corrupted file ["+corruptedFileName+"]."); + } + } + + + if (file.renameTo(corruptedFile)) + { + LOGGER.error("Renamed corrupted file to ["+corruptedFileName+"]."); + } + else + { + LOGGER.error("Failed to rename corrupted file to ["+corruptedFileName+"]. Attempting to delete file..."); + if (!file.delete()) + { + LOGGER.error("Unable to delete corrupted file ["+corruptedFileName+"]."); + } + } + + return corruptedFile; + } + + +} diff --git a/core/src/main/java/com/seibel/lod/core/file/datafile/DataFileHandler.java b/core/src/main/java/com/seibel/lod/core/file/datafile/DataFileHandler.java index 8d73831eb..38884a806 100644 --- a/core/src/main/java/com/seibel/lod/core/file/datafile/DataFileHandler.java +++ b/core/src/main/java/com/seibel/lod/core/file/datafile/DataFileHandler.java @@ -264,13 +264,29 @@ public class DataFileHandler implements IDataSourceProvider @Override public CompletableFuture read(DhSectionPos pos) { - this.topDetailLevel.updateAndGet(v -> Math.max(v, pos.sectionDetailLevel)); + this.topDetailLevel.updateAndGet(intVal -> Math.max(intVal, pos.sectionDetailLevel)); DataMetaFile metaFile = this.getOrMakeFile(pos); if (metaFile == null) { return CompletableFuture.completedFuture(null); } - return metaFile.loadOrGetCached(); + + + // future wrapper necessary in order to handle file read errors + CompletableFuture futureWrapper = new CompletableFuture<>(); + metaFile.loadOrGetCachedAsync().exceptionally((e) -> + { + DataMetaFile newMetaFile = this.removeCorruptedFile(pos, metaFile, e); + + futureWrapper.completeExceptionally(e); + return null; // return value doesn't matter + }) + .whenComplete((dataSource, e) -> + { + futureWrapper.complete(dataSource); + }); + + return futureWrapper; } /** This call is concurrent. I.e. it supports being called by multiple threads at the same time. */ @@ -363,8 +379,7 @@ public class DataFileHandler implements IDataSourceProvider for (DataMetaFile metaFile : existFiles) { - futures.add(f.loadOrGetCached() - .exceptionally((ex) -> null) + futures.add(metaFile.loadOrGetCachedAsync() .thenAccept((data) -> { if (data != null) @@ -373,14 +388,30 @@ public class DataFileHandler implements IDataSourceProvider dataSource.sampleFrom(data); } }) + .exceptionally((e) -> + { + DataMetaFile newMetaFile = this.removeCorruptedFile(pos, metaFile, e); + return null; + }) ); } return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)) .thenApply((v) -> dataSource.trySelfPromote()); + } } + private DataMetaFile removeCorruptedFile(DhSectionPos pos, DataMetaFile metaFile, Throwable exception) + { + LOGGER.error("Error reading Data file ["+pos+"]", exception); + + FileUtil.renameCorruptedFile(metaFile.path); + // remove the DataMetaFile since the old one was corrupted + this.files.remove(pos); + // create a new DataMetaFile to write new data to + return this.getOrMakeFile(pos); + } - @Override + @Override public ILodDataSource onDataFileLoaded(ILodDataSource source, MetaData metaData, Consumer onUpdated, Function updater) { @@ -426,17 +457,10 @@ public class DataFileHandler implements IDataSourceProvider } @Override - public File computeDataFilePath(DhSectionPos pos) - { - return new File(this.saveDir, pos.serialize() + ".lod"); - } + public File computeDataFilePath(DhSectionPos pos) { return new File(this.saveDir, pos.serialize() + ".lod"); } @Override - public Executor getIOExecutor() - { - return this.fileReaderThread; - } - + public Executor getIOExecutor() { return this.fileReaderThread; } @Override public void close() diff --git a/core/src/main/java/com/seibel/lod/core/file/datafile/DataMetaFile.java b/core/src/main/java/com/seibel/lod/core/file/datafile/DataMetaFile.java index 56eaf6a25..19c1ffdf5 100644 --- a/core/src/main/java/com/seibel/lod/core/file/datafile/DataMetaFile.java +++ b/core/src/main/java/com/seibel/lod/core/file/datafile/DataMetaFile.java @@ -151,22 +151,22 @@ public class DataMetaFile extends AbstractMetaDataFile { debugCheck(); Object obj = this.data.get(); - + CompletableFuture cached = this._readCachedAsync(obj); if (cached != null) { return cached; } - + CompletableFuture future = new CompletableFuture<>(); - + // Would use faster and non-nesting Compare and exchange. But java 8 doesn't have it! :( - boolean worked = this.data.compareAndSet(obj, future); + boolean worked = this.data.compareAndSet(obj, future); // TODO obj and future are different object types, would this ever return true? if (!worked) { return this.loadOrGetCachedAsync(); } - + // After cas. We are in exclusive control. if (!this.doesFileExist) { @@ -181,6 +181,7 @@ public class DataMetaFile extends AbstractMetaDataFile { if (e != null) { + LOGGER.error("Uncaught error on creation {}: ", this.path, e); future.complete(null); this.data.set(null); } @@ -198,7 +199,7 @@ public class DataMetaFile extends AbstractMetaDataFile { if (this.metaData == null) { - throw new IllegalStateException("Meta data not loaded!"); + throw new IllegalStateException("Meta data not loaded!"); // TODO should this be a CompletionException? } // Load the file. @@ -207,10 +208,12 @@ public class DataMetaFile extends AbstractMetaDataFile { data = this.loader.loadData(this, fio, this.level); } - catch (IOException e) + catch (Exception e) { + // can happen if there is a missing file or the file was incorrectly formatted throw new CompletionException(e); } + // Apply the write queue LodUtil.assertTrue(this.inCacheWriteAccessFuture.get() == null, "No one should be writing to the cache while we are in the process of " + @@ -218,17 +221,20 @@ public class DataMetaFile extends AbstractMetaDataFile data = this.handler.onDataFileLoaded(data, this.metaData, this::saveChanges, this::applyWriteQueue); return data; - }, handler.getIOExecutor()) - .whenComplete((f, e) -> { - if (e != null) { - LOGGER.error("Error loading file {}: ", path, e); - future.complete(null); - data.set(null); - } else { - future.complete(f); - new DataObjTracker(f); - this.data.set(new SoftReference<>(f)); - } + }, this.handler.getIOExecutor()) + .exceptionally((e) -> + { + LOGGER.error("Error loading file {}: ", this.path, e); + this.data.set(null); + + future.completeExceptionally(e); + return null; // the return value here doesn't matter + }) + .whenComplete((dataSource, e) -> + { + future.complete(dataSource); + new DataObjTracker(dataSource); + this.data.set(new SoftReference<>(dataSource)); }); } diff --git a/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderFileHandler.java b/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderFileHandler.java index 36fe052be..81e7870ed 100644 --- a/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderFileHandler.java +++ b/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderFileHandler.java @@ -290,14 +290,10 @@ public class RenderFileHandler implements ILodRenderSourceProvider return dataSource; }).exceptionally((ex) -> { - if (ex != null) - { - LOGGER.error("Uncaught exception when getting data for updateCache()", ex); - } - + LOGGER.error("Exception when getting data for updateCache()", ex); return null; }); - + LOGGER.info("Recreating cache for {}", data.getSectionPos()); DataRenderTransformer.asyncTransformDataSource(dataFuture, this.level) .thenAccept((newRenderDataSource) -> this.write(dataRef.get(), file, newRenderDataSource, this.dataSourceProvider.getCacheVersion(data.getSectionPos()))) @@ -310,7 +306,7 @@ public class RenderFileHandler implements ILodRenderSourceProvider return null; }).thenRun(() -> this.cacheUpdateLockBySectionPos.remove(file.pos)); } - + public ILodRenderSource onRenderFileLoaded(ILodRenderSource data, RenderMetaDataFile file) { if (!this.dataSourceProvider.isCacheVersionValid(file.pos, file.metaData.dataVersion.get()))